WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"path"
"strings"
"github.com/Tencent/WeKnora/internal/agent/skills"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/sandbox"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/utils"
)
// Tool name constant for execute_skill_script
var executeSkillScriptTool = BaseTool{
name: ToolExecuteSkillScript,
description: `Execute a script with a skill's own interpreter and dependencies.
## Working Directory
- The script runs with ` + "`/workspace`" + ` as its working directory, whichever
skill it belongs to. A relative path the script itself opens resolves there,
not inside the skill; ` + "`$WEKNORA_SKILL_DIR`" + ` is how a script reaches the
files installed beside it.
- Generated files belong in ` + "`$WEKNORA_SKILL_OUTPUT_DIR`" + `, which is
collected for download. ` + "`/workspace/input`" + ` is read-only.
## Usage
- ` + "`script_path`" + ` is either:
- a path **inside the skill** (` + "`scripts/analyze.py`" + `), or
- an absolute session file you just wrote with ` + "`write_sandbox_file`" + `
(` + "`/workspace/output/generate_ppt.py`" + `). That file still runs with
this skill's virtualenv / node_modules.
- Do NOT pass ` + "`/workspace/input`" + ` paths as ` + "`script_path`" + `
(attachments are inputs via ` + "`args`" + `).
- User-uploaded files are listed in the current ` + "`<sandbox_attachments>`" + `
block. Pass their absolute ` + "`/workspace/input/...`" + ` paths through
` + "`args`" + ` when a script accepts an input file.
- Treat ` + "`/workspace/input`" + ` as read-only. Write generated files only to
` + "`$WEKNORA_SKILL_OUTPUT_DIR`" + ` so they can be collected for download.
- Scripts reach the dependencies their install put beside them: Python runs
with the skill's own virtualenv interpreter, Node resolves the skill's
node_modules from the script's location. A failed import under a bare
` + "`python3 -c`" + ` or ` + "`node -e`" + ` says nothing about whether this
tool can run the script.
- The skill tree is frozen after install. Do not run ` + "`install_deps.py`" + `
(or ` + "`python -m pip`" + ` / ` + "`ensurepip`" + ` inside the skill ` + "`.venv`" + `) to
fetch extras at chat time. If a package is missing, install it into
` + "`/workspace/.skill-packages/<skill_name>`" + ` with system ` + "`python3 -m pip install --target`" + `
and call this tool again — PYTHONPATH already includes that directory.
## When to Use
- When a skill's instructions reference a utility script (e.g., "Run scripts/analyze_form.py")
- After ` + "`write_sandbox_file`" + ` / ` + "`edit_sandbox_file`" + ` customizes a skill
script and you still need that skill's packages (python-pptx, pandas, …)
- When automation or data processing is needed as part of skill workflow
- For deterministic operations where script execution is more reliable than generating code
## When NOT to Use
- Do not use this for a standalone ` + "`/workspace`" + ` script that does not need a
skill's packages — call ` + "`shell_exec`" + ` instead.
## Security
- Scripts run in a sandboxed environment with limited permissions
- Network access is disabled by default
- Bundled scripts stay inside the skill directory; session files must sit
under ` + "`/workspace`" + ` and not under ` + "`/workspace/input`" + `
## Returns
- Script stdout and stderr output
- Exit code indicating success (0) or failure (non-zero)`,
schema: utils.GenerateSchema[ExecuteSkillScriptInput](),
}
// ExecuteSkillScriptInput defines the input parameters for the execute_skill_script tool
type ExecuteSkillScriptInput struct {
SkillName string `json:"skill_name" jsonschema:"Name of the skill containing the script"`
ScriptPath string `json:"script_path" jsonschema:"Relative path inside the skill (e.g. scripts/analyze.py), or an absolute /workspace/... file written with write_sandbox_file. Do not pass /workspace/input paths."`
Args []string `json:"args,omitempty" jsonschema:"Optional command-line arguments. For file flags, pass an absolute path from the current <sandbox_attachments> block (/workspace/input/...). For in-memory data, use input instead."`
Input string `json:"input,omitempty" jsonschema:"Optional input data to pass to the script via stdin. Use this when you have data in memory (e.g. JSON string) that the script should process. This is equivalent to piping data: echo 'data' | python script.py"`
}
// UnmarshalJSON accepts args as either the documented string array or a single
// command-line string. Some model providers emit a string for a single tool
// argument; accepting it here keeps that malformed-but-unambiguous call from
// failing before the script can run.
func (i *ExecuteSkillScriptInput) UnmarshalJSON(data []byte) error {
var raw struct {
SkillName string `json:"skill_name"`
ScriptPath string `json:"script_path"`
Args json.RawMessage `json:"args"`
Input string `json:"input"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
i.SkillName = raw.SkillName
i.ScriptPath = raw.ScriptPath
i.Input = raw.Input
i.Args = nil
if len(raw.Args) == 0 || string(raw.Args) == "null" {
return nil
}
if err := json.Unmarshal(raw.Args, &i.Args); err == nil {
return nil
}
var argsString string
if err := json.Unmarshal(raw.Args, &argsString); err != nil {
return fmt.Errorf("args must be a string or an array of strings: %w", err)
}
// Some providers emit the array as a stringified JSON payload
// (e.g. "[\"--project-name\",\"X\"]"). Treat that as an array first so the
// model's intent is preserved; strings.Fields would otherwise split the
// brackets/quotes into garbage tokens and the script would see nonsense argv.
if err := json.Unmarshal([]byte(argsString), &i.Args); err == nil {
return nil
}
// A plain string is interpreted as a conventional space-separated command
// line. The tool schema continues to advertise []string, so well-formed
// calls are unaffected; this is only a compatibility fallback for model
// output.
i.Args = strings.Fields(argsString)
return nil
}
// ExecuteSkillScriptTool allows the agent to execute skill scripts in a sandbox
type ExecuteSkillScriptTool struct {
BaseTool
skillManager *skills.Manager
}
// NewExecuteSkillScriptTool creates a new execute_skill_script tool instance
func NewExecuteSkillScriptTool(skillManager *skills.Manager) *ExecuteSkillScriptTool {
return &ExecuteSkillScriptTool{
BaseTool: executeSkillScriptTool,
skillManager: skillManager,
}
}
// Execute executes the execute_skill_script tool
func (t *ExecuteSkillScriptTool) Execute(ctx context.Context, args json.RawMessage) (*types.ToolResult, error) {
logger.Infof(ctx, "[Tool][ExecuteSkillScript] Execute started")
// Parse input
var input ExecuteSkillScriptInput
if err := json.Unmarshal(args, &input); err != nil {
logger.Errorf(ctx, "[Tool][ExecuteSkillScript] Failed to parse args: %v", err)
return &types.ToolResult{
Success: false,
Error: fmt.Sprintf("Failed to parse args: %v", err),
}, nil
}
// Validate required fields
if input.SkillName == "" {
return &types.ToolResult{
Success: false,
Error: "skill_name is required",
}, nil
}
if input.ScriptPath == "" {
return &types.ToolResult{
Success: false,
Error: "script_path is required",
}, nil
}
if _, ok := sandbox.RunnableWorkspaceScript(input.ScriptPath); !ok {
rel, err := skillRelativeFilePath(input.SkillName, input.ScriptPath)
if err != nil {
return &types.ToolResult{
Success: false,
Error: err.Error(),
}, nil
}
if rel == "" {
return &types.ToolResult{
Success: false,
Error: "script_path is the skill directory; pass a relative script such as scripts/generate_ppt.py",
}, nil
}
input.ScriptPath = rel
}
// Check if skill manager is available
if t.skillManager == nil || !t.skillManager.IsEnabled() {
return &types.ToolResult{
Success: false,
Error: "Skills are not enabled",
}, nil
}
// Execute the script in sandbox
logger.Infof(ctx, "[Tool][ExecuteSkillScript] Executing script: %s/%s with args: %v, input length: %d",
input.SkillName, input.ScriptPath, input.Args, len(input.Input))
result, err := t.skillManager.ExecuteScript(ctx, input.SkillName, input.ScriptPath, input.Args, input.Input)
if err != nil {
logger.Errorf(ctx, "[Tool][ExecuteSkillScript] Script execution failed: %v", err)
return &types.ToolResult{
Success: false,
Error: fmt.Sprintf("Script execution failed: %v", err),
}, nil
}
// Build output
var builder strings.Builder
builder.WriteString(fmt.Sprintf("=== Script Execution: %s/%s ===\n\n", input.SkillName, input.ScriptPath))
if len(input.Args) > 0 {
builder.WriteString(fmt.Sprintf("**Arguments**: %v\n", input.Args))
}
builder.WriteString(fmt.Sprintf("**Exit Code**: %d\n", result.ExitCode))
builder.WriteString(fmt.Sprintf("**Duration**: %v\n\n", result.Duration))
if result.Killed {
builder.WriteString("**Warning**: Script was terminated (timeout or killed)\n\n")
}
if result.Stdout != "" {
builder.WriteString("## Standard Output\n\n")
builder.WriteString("```\n")
builder.WriteString(result.Stdout)
if !strings.HasSuffix(result.Stdout, "\n") {
builder.WriteString("\n")
}
builder.WriteString("```\n\n")
}
if result.Stderr != "" {
builder.WriteString("## Standard Error\n\n")
builder.WriteString("```\n")
builder.WriteString(result.Stderr)
if !strings.HasSuffix(result.Stderr, "\n") {
builder.WriteString("\n")
}
builder.WriteString("```\n\n")
}
if result.Error != "" {
builder.WriteString("## Error\n\n")
builder.WriteString(result.Error)
builder.WriteString("\n")
}
if hint := skillOnDemandInstallHint(input.SkillName, input.ScriptPath, result.Stdout, result.Stderr); hint != "" {
builder.WriteString(hint)
builder.WriteString("\n")
} else if !result.IsSuccess() {
if hint := pythonSyntaxErrorHint(result.Stderr); hint != "" {
builder.WriteString(hint)
builder.WriteString("\n")
} else if hint := skillMissingPackageHint(input.SkillName, result.Stderr); hint != "" {
builder.WriteString(hint)
builder.WriteString("\n")
}
}
// Determine success based on exit code
success := result.IsSuccess()
resultData := map[string]interface{}{
"display_type": "shell_exec",
"command": skillScriptCommand(input),
"skill_name": input.SkillName,
"script_path": input.ScriptPath,
"args": input.Args,
"exit_code": result.ExitCode,
"stdout": result.Stdout,
"stderr": result.Stderr,
"duration_ms": result.Duration.Milliseconds(),
"killed": result.Killed,
}
logger.Infof(ctx, "[Tool][ExecuteSkillScript] Script completed with exit code: %d", result.ExitCode)
return &types.ToolResult{
Success: success,
Output: builder.String(),
Data: resultData,
Error: func() string {
if !success {
if result.Error != "" {
return result.Error
}
return fmt.Sprintf("Script exited with code %d", result.ExitCode)
}
return ""
}(),
}, nil
}
func skillScriptCommand(input ExecuteSkillScriptInput) string {
parts := make([]string, 0, 1+len(input.Args))
script := strings.TrimSpace(input.ScriptPath)
switch {
case strings.HasPrefix(path.Clean(script), "/workspace/"):
parts = append(parts, script)
case input.SkillName != "" && script != "":
parts = append(parts, input.SkillName+"/"+script)
case script != "":
parts = append(parts, script)
}
parts = append(parts, input.Args...)
return strings.Join(parts, " ")
}
// Cleanup releases any resources
func (t *ExecuteSkillScriptTool) Cleanup(ctx context.Context) error {
return nil
}