@downcity/agent 1.1.96 → 1.1.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/executor/composer/system/default/assets/core.prompt.d.ts +1 -1
- package/bin/executor/composer/system/default/assets/core.prompt.d.ts.map +1 -1
- package/bin/executor/composer/system/default/assets/core.prompt.js +1 -1
- package/bin/executor/composer/system/default/assets/core.prompt.js.map +1 -1
- package/bin/executor/tools/shell/ShellToolBridge.d.ts.map +1 -1
- package/bin/executor/tools/shell/ShellToolBridge.js +14 -0
- package/bin/executor/tools/shell/ShellToolBridge.js.map +1 -1
- package/bin/executor/tools/shell/types/ShellPlugin.d.ts +8 -0
- package/bin/executor/tools/shell/types/ShellPlugin.d.ts.map +1 -1
- package/bin/index.d.ts +1 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js.map +1 -1
- package/bin/plugin/core/ImagePlugin.d.ts +3 -64
- package/bin/plugin/core/ImagePlugin.d.ts.map +1 -1
- package/bin/plugin/core/ImagePlugin.js +12 -232
- package/bin/plugin/core/ImagePlugin.js.map +1 -1
- package/bin/sandbox/LinuxBubblewrapSandbox.d.ts +1 -3
- package/bin/sandbox/LinuxBubblewrapSandbox.d.ts.map +1 -1
- package/bin/sandbox/LinuxBubblewrapSandbox.js +31 -30
- package/bin/sandbox/LinuxBubblewrapSandbox.js.map +1 -1
- package/bin/sandbox/MacOsSeatbeltSandbox.d.ts +1 -1
- package/bin/sandbox/MacOsSeatbeltSandbox.d.ts.map +1 -1
- package/bin/sandbox/MacOsSeatbeltSandbox.js +30 -29
- package/bin/sandbox/MacOsSeatbeltSandbox.js.map +1 -1
- package/bin/sandbox/SandboxConfigResolver.d.ts +1 -0
- package/bin/sandbox/SandboxConfigResolver.d.ts.map +1 -1
- package/bin/sandbox/SandboxConfigResolver.js +13 -3
- package/bin/sandbox/SandboxConfigResolver.js.map +1 -1
- package/bin/sandbox/SandboxRunner.d.ts +17 -4
- package/bin/sandbox/SandboxRunner.d.ts.map +1 -1
- package/bin/sandbox/SandboxRunner.js +20 -5
- package/bin/sandbox/SandboxRunner.js.map +1 -1
- package/bin/sandbox/types/SandboxRuntime.d.ts +46 -6
- package/bin/sandbox/types/SandboxRuntime.d.ts.map +1 -1
- package/bin/sandbox/types/SandboxRuntime.js +2 -2
- package/bin/types/plugin/ImagePlugin.d.ts +2 -79
- package/bin/types/plugin/ImagePlugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/scripts/image-plugin-job.test.mjs +21 -108
- package/scripts/linux-bubblewrap-sandbox.test.mjs +23 -14
- package/src/executor/composer/system/default/assets/core.prompt.ts +1 -1
- package/src/executor/composer/system/default/assets/core.prompt.ts.txt +5 -0
- package/src/executor/tools/shell/ShellToolBridge.ts +14 -0
- package/src/executor/tools/shell/types/ShellPlugin.ts +8 -0
- package/src/index.ts +0 -5
- package/src/plugin/core/ImagePlugin.ts +13 -286
- package/src/sandbox/LinuxBubblewrapSandbox.ts +35 -43
- package/src/sandbox/MacOsSeatbeltSandbox.ts +35 -41
- package/src/sandbox/SandboxConfigResolver.ts +15 -3
- package/src/sandbox/SandboxRunner.ts +32 -7
- package/src/sandbox/types/SandboxRuntime.ts +54 -6
- package/src/types/plugin/ImagePlugin.ts +2 -79
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -19,26 +19,29 @@ async function createSandboxFixture() {
|
|
|
19
19
|
const projectRoot = path.join(root, "project");
|
|
20
20
|
const writablePath = path.join(projectRoot, ".downcity");
|
|
21
21
|
const shellDir = path.join(writablePath, "shell", "sh_test");
|
|
22
|
-
const
|
|
23
|
-
const
|
|
22
|
+
const sandboxDir = path.join(projectRoot, ".downcity", "sandbox");
|
|
23
|
+
const tmpDir = path.join(sandboxDir, "tmp");
|
|
24
|
+
const cacheDir = path.join(sandboxDir, ".cache");
|
|
24
25
|
|
|
25
|
-
await fs.mkdir(
|
|
26
|
-
await fs.mkdir(
|
|
26
|
+
await fs.mkdir(shellDir, { recursive: true });
|
|
27
|
+
await fs.mkdir(tmpDir, { recursive: true });
|
|
28
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
27
29
|
|
|
28
30
|
return {
|
|
29
31
|
root,
|
|
30
32
|
projectRoot,
|
|
31
33
|
writablePath,
|
|
32
34
|
shellDir,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
sandboxDir,
|
|
36
|
+
tmpDir,
|
|
37
|
+
cacheDir,
|
|
35
38
|
};
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
function createParams(fixture, overrides = {}) {
|
|
39
42
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
executionId: "sh_test",
|
|
44
|
+
executionDir: fixture.shellDir,
|
|
42
45
|
cmd: "printf hello",
|
|
43
46
|
cwd: fixture.projectRoot,
|
|
44
47
|
actualCwd: fixture.projectRoot,
|
|
@@ -52,12 +55,14 @@ function createParams(fixture, overrides = {}) {
|
|
|
52
55
|
config: {
|
|
53
56
|
backend: "linux-bubblewrap",
|
|
54
57
|
rootPath: fixture.projectRoot,
|
|
58
|
+
sandboxDir: fixture.sandboxDir,
|
|
59
|
+
homeDir: fixture.sandboxDir,
|
|
60
|
+
tmpDir: fixture.tmpDir,
|
|
61
|
+
cacheDir: fixture.cacheDir,
|
|
55
62
|
envAllowlist: ["PATH", "LANG"],
|
|
56
|
-
writablePaths: [fixture.
|
|
63
|
+
writablePaths: [fixture.projectRoot, fixture.sandboxDir],
|
|
57
64
|
networkMode: "off",
|
|
58
65
|
},
|
|
59
|
-
shellHomeDir: fixture.shellHomeDir,
|
|
60
|
-
shellTmpDir: fixture.shellTmpDir,
|
|
61
66
|
...overrides,
|
|
62
67
|
};
|
|
63
68
|
}
|
|
@@ -96,9 +101,9 @@ test("Linux bubblewrap args isolate network and overlay writable project paths",
|
|
|
96
101
|
assert.equal(hasArg(args, "--die-with-parent"), true);
|
|
97
102
|
assert.equal(hasArg(args, "--unshare-pid"), true);
|
|
98
103
|
assert.equal(hasArg(args, "--unshare-net"), true);
|
|
99
|
-
assert.equal(hasOptionPair(args, "--
|
|
100
|
-
assert.equal(hasOptionPair(args, "--bind", fixture.
|
|
101
|
-
assert.equal(hasOptionPair(args, "--bind", fixture.projectRoot), false);
|
|
104
|
+
assert.equal(hasOptionPair(args, "--bind", fixture.projectRoot), true);
|
|
105
|
+
assert.equal(hasOptionPair(args, "--bind", fixture.sandboxDir), false);
|
|
106
|
+
assert.equal(hasOptionPair(args, "--ro-bind", fixture.projectRoot), false);
|
|
102
107
|
assert.equal(hasOptionValue(args, "--dir", fixture.writablePath), false);
|
|
103
108
|
assert.deepEqual(args.slice(-5), [
|
|
104
109
|
"--chdir",
|
|
@@ -119,6 +124,10 @@ test("Linux bubblewrap args keep root writable when sandbox writablePaths includ
|
|
|
119
124
|
config: {
|
|
120
125
|
backend: "linux-bubblewrap",
|
|
121
126
|
rootPath: fixture.projectRoot,
|
|
127
|
+
sandboxDir: fixture.sandboxDir,
|
|
128
|
+
homeDir: fixture.sandboxDir,
|
|
129
|
+
tmpDir: fixture.tmpDir,
|
|
130
|
+
cacheDir: fixture.cacheDir,
|
|
122
131
|
envAllowlist: ["PATH"],
|
|
123
132
|
writablePaths: [fixture.projectRoot],
|
|
124
133
|
networkMode: "full",
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Source: src/executor/composer/system/default/assets/core.prompt.ts.txt
|
|
7
|
-
const TEXT_MODULE_CONTENT = "你拥有且仅拥有当前项目 {{project_path}} 的使用权和修改权。当前年份是 {{current_year}} 年。\n1. `.downcity/` 是 Downcity 的运行时数据目录(通常不需要你手动读取/修改;系统会自动写入与注入)。结构与逻辑如下:\n - `.downcity/agents/<agentId>/sessions/` 是会话消息。\n - `.downcity/memory/` 是中长期记忆。\n - `.downcity/profile/Primary.md`、`.downcity/profile/other.md`:全局 profile 记忆;存在时会自动作为 system prompt 注入。\n - `.downcity/public/`:对外静态资源目录,通过 `GET /downcity/public/<path>` 访问;用于给外部访问的路径。不要存放敏感信息,Town Agent HTTP gateway 会把 `.downcity/public/` 暴露为 HTTP 静态资源:`GET /downcity/public/<path>`,你可以把该 URL 发给用户用于下载/查看生成的文件(注意不要暴露敏感信息)。\n - `.downcity/logs/<YYYY-MM-DD>.jsonl`:运行日志(JSONL);用于排查问题,避免把原始日志整段贴给用户。\n - `.downcity/.cache/`:幂等/去重缓存(ingress/egress);不要手动改。\n - `.downcity/.debug/`:调试产物(Town 托管进程 pid/log/meta、适配器事件抓取等);仅在排查问题时查看。\n - `.downcity/data/`:小型持久化数据(预留)。\n - `.downcity/task/`:Task 目录。\n2. PROFILE.md + SOUL.md + downcity.json 是你的一些配置文件,你不需要读取。\n\n# 最重要\n【关于命令执行工具】(重要)\n- 短命令、一次性命令优先使用 `shell_exec`。\n- 长任务、需要中途查状态、需要 stdin 交互时,使用 `shell_start` / `shell_status` / `shell_read` / `shell_write` / `shell_wait` / `shell_close`。\n- 先用 `shell_start` 启动命令并拿到 `shell_id`。\n- `shell_id` 是 shell 会话标识;它不是 chat `session_id`。\n- 长任务期间,优先使用 `shell_status` 查询进度,或使用 `shell_wait` 等待状态变化;不要自己写高频空轮询循环。\n- 只有在确实需要原始增量输出时,才使用 `shell_read` 按 `from_cursor` 继续读取。\n- 需要向进程 stdin 输入内容时,使用 `shell_write`。\n- 命令会话完成后若不再需要,使用 `shell_close` 主动释放资源。\n- 不要把原始超长 shell 输出直接转发给用户,应先总结。\n\n# 默认决策与澄清\n- 默认先执行,再沟通:对低风险、可回滚、用户意图已经足够明显的请求,优先基于当前日期、时区、聊天上下文与常见默认值直接执行,不要在事件标题、默认平台、显然的时间表达上反复追问。\n- 只有当“缺失信息会实质改变结果”时才追问;例如:会影响日期/对象/金额/账户/发送目标,或会触发不可逆、高风险、涉隐私操作。\n- 处理时间表达时,优先使用当前环境提供的 `current_date`、`current_time` 与 `timezone`;如果入站 `<info>` 明确提供了 `user_timezone`,则优先按 `user_timezone` 解析,否则按 runtime clock 的 `timezone` 解析。像“今天/明天/下午两点/提前两小时”这类表达,应先解析为绝对时间,再执行,并在回复里明确写出绝对日期时间。\n- 当任务依赖外部权限、系统能力或第三方连接(如日历、提醒事项、聊天渠道、系统授权)时,先探测可用性,再决定是否承诺“我来创建/发送/写入”。\n- 如果探测结果显示被系统权限、宿主环境或连接状态阻塞,要直接说明真实阻塞点和下一步,而不是先给出“可以,我来做”的承诺后再多轮追问。\n- 若已经有足够信息可以一次完成多个低风险默认动作,应直接完成,并在结果里简短说明采用了哪些默认假设。\n\n# 很重要\n\n安全与边界\n- 不要执行破坏性命令(如 `rm -rf`、`git reset --hard`)除非用户明确要求。\n- 遇到 API Key、Token、Secret、环境变量、bot 凭据等密钥管理问题时,优先指导用户使用 Console(如 `Global / Env`、`Global / Channel Accounts`)维护,不要要求用户把密钥明文直接发送到当前聊天里。\n- `town keys` 只能列出已配置的 key 名与描述,不会返回密钥值。不要让用户把密钥“发给你自己”或继续尝试通过 `town keys` 获取明文。\n";
|
|
7
|
+
const TEXT_MODULE_CONTENT = "你拥有且仅拥有当前项目 {{project_path}} 的使用权和修改权。当前年份是 {{current_year}} 年。\n1. `.downcity/` 是 Downcity 的运行时数据目录(通常不需要你手动读取/修改;系统会自动写入与注入)。结构与逻辑如下:\n - `.downcity/agents/<agentId>/sessions/` 是会话消息。\n - `.downcity/memory/` 是中长期记忆。\n - `.downcity/profile/Primary.md`、`.downcity/profile/other.md`:全局 profile 记忆;存在时会自动作为 system prompt 注入。\n - `.downcity/public/`:对外静态资源目录,通过 `GET /downcity/public/<path>` 访问;用于给外部访问的路径。不要存放敏感信息,Town Agent HTTP gateway 会把 `.downcity/public/` 暴露为 HTTP 静态资源:`GET /downcity/public/<path>`,你可以把该 URL 发给用户用于下载/查看生成的文件(注意不要暴露敏感信息)。\n - `.downcity/logs/<YYYY-MM-DD>.jsonl`:运行日志(JSONL);用于排查问题,避免把原始日志整段贴给用户。\n - `.downcity/.cache/`:幂等/去重缓存(ingress/egress);不要手动改。\n - `.downcity/.debug/`:调试产物(Town 托管进程 pid/log/meta、适配器事件抓取等);仅在排查问题时查看。\n - `.downcity/data/`:小型持久化数据(预留)。\n - `.downcity/task/`:Task 目录。\n - `.downcity/sandbox/`:当前 agent 的本地命令执行 sandbox HOME/cache/tmp;shell 与 script 命令会共享它。\n2. PROFILE.md + SOUL.md + downcity.json 是你的一些配置文件,你不需要读取。\n\n# 最重要\n【关于命令执行工具】(重要)\n- 短命令、一次性命令优先使用 `shell_exec`。\n- 长任务、需要中途查状态、需要 stdin 交互时,使用 `shell_start` / `shell_status` / `shell_read` / `shell_write` / `shell_wait` / `shell_close`。\n- 先用 `shell_start` 启动命令并拿到 `shell_id`。\n- `shell_id` 是 shell 会话标识;它不是 chat `session_id`。\n- 长任务期间,优先使用 `shell_status` 查询进度,或使用 `shell_wait` 等待状态变化;不要自己写高频空轮询循环。\n- 只有在确实需要原始增量输出时,才使用 `shell_read` 按 `from_cursor` 继续读取。\n- 需要向进程 stdin 输入内容时,使用 `shell_write`。\n- 命令会话完成后若不再需要,使用 `shell_close` 主动释放资源。\n- 不要把原始超长 shell 输出直接转发给用户,应先总结。\n- shell 命令默认在当前 agent 的 sandbox 中执行:项目目录可读写,网络可用,HOME 指向 `.downcity/sandbox/`,真实用户 HOME 与系统目录不可写。\n- 安装 Python 依赖时优先使用项目内 `.venv`,不要使用 `pip install --user`。\n- 不要尝试 `sudo`、`brew install`、Xcode Command Line Tools 安装或写 `/usr/local`、`/opt/homebrew`、`/System` 等宿主系统目录;如果确实缺少系统级依赖,直接告诉用户需要在宿主机安装。\n- 下载模型、工具缓存、临时状态应自然落在 `.downcity/sandbox/` 或项目目录中,不要假设可复用真实用户缓存。\n\n# 默认决策与澄清\n- 默认先执行,再沟通:对低风险、可回滚、用户意图已经足够明显的请求,优先基于当前日期、时区、聊天上下文与常见默认值直接执行,不要在事件标题、默认平台、显然的时间表达上反复追问。\n- 只有当“缺失信息会实质改变结果”时才追问;例如:会影响日期/对象/金额/账户/发送目标,或会触发不可逆、高风险、涉隐私操作。\n- 处理时间表达时,优先使用当前环境提供的 `current_date`、`current_time` 与 `timezone`;如果入站 `<info>` 明确提供了 `user_timezone`,则优先按 `user_timezone` 解析,否则按 runtime clock 的 `timezone` 解析。像“今天/明天/下午两点/提前两小时”这类表达,应先解析为绝对时间,再执行,并在回复里明确写出绝对日期时间。\n- 当任务依赖外部权限、系统能力或第三方连接(如日历、提醒事项、聊天渠道、系统授权)时,先探测可用性,再决定是否承诺“我来创建/发送/写入”。\n- 如果探测结果显示被系统权限、宿主环境或连接状态阻塞,要直接说明真实阻塞点和下一步,而不是先给出“可以,我来做”的承诺后再多轮追问。\n- 若已经有足够信息可以一次完成多个低风险默认动作,应直接完成,并在结果里简短说明采用了哪些默认假设。\n\n# 很重要\n\n安全与边界\n- 不要执行破坏性命令(如 `rm -rf`、`git reset --hard`)除非用户明确要求。\n- 遇到 API Key、Token、Secret、环境变量、bot 凭据等密钥管理问题时,优先指导用户使用 Console(如 `Global / Env`、`Global / Channel Accounts`)维护,不要要求用户把密钥明文直接发送到当前聊天里。\n- `town keys` 只能列出已配置的 key 名与描述,不会返回密钥值。不要让用户把密钥“发给你自己”或继续尝试通过 `town keys` 获取明文。\n";
|
|
8
8
|
|
|
9
9
|
export default TEXT_MODULE_CONTENT;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- `.downcity/.debug/`:调试产物(Town 托管进程 pid/log/meta、适配器事件抓取等);仅在排查问题时查看。
|
|
10
10
|
- `.downcity/data/`:小型持久化数据(预留)。
|
|
11
11
|
- `.downcity/task/`:Task 目录。
|
|
12
|
+
- `.downcity/sandbox/`:当前 agent 的本地命令执行 sandbox HOME/cache/tmp;shell 与 script 命令会共享它。
|
|
12
13
|
2. PROFILE.md + SOUL.md + downcity.json 是你的一些配置文件,你不需要读取。
|
|
13
14
|
|
|
14
15
|
# 最重要
|
|
@@ -22,6 +23,10 @@
|
|
|
22
23
|
- 需要向进程 stdin 输入内容时,使用 `shell_write`。
|
|
23
24
|
- 命令会话完成后若不再需要,使用 `shell_close` 主动释放资源。
|
|
24
25
|
- 不要把原始超长 shell 输出直接转发给用户,应先总结。
|
|
26
|
+
- shell 命令默认在当前 agent 的 sandbox 中执行:项目目录可读写,网络可用,HOME 指向 `.downcity/sandbox/`,真实用户 HOME 与系统目录不可写。
|
|
27
|
+
- 安装 Python 依赖时优先使用项目内 `.venv`,不要使用 `pip install --user`。
|
|
28
|
+
- 不要尝试 `sudo`、`brew install`、Xcode Command Line Tools 安装或写 `/usr/local`、`/opt/homebrew`、`/System` 等宿主系统目录;如果确实缺少系统级依赖,直接告诉用户需要在宿主机安装。
|
|
29
|
+
- 下载模型、工具缓存、临时状态应自然落在 `.downcity/sandbox/` 或项目目录中,不要假设可复用真实用户缓存。
|
|
25
30
|
|
|
26
31
|
# 默认决策与澄清
|
|
27
32
|
- 默认先执行,再沟通:对低风险、可回滚、用户意图已经足够明显的请求,优先基于当前日期、时区、聊天上下文与常见默认值直接执行,不要在事件标题、默认平台、显然的时间表达上反复追问。
|
|
@@ -211,6 +211,13 @@ export function flattenShellActionResponse(params: {
|
|
|
211
211
|
status: shell.status,
|
|
212
212
|
cmd: shell.cmd,
|
|
213
213
|
cwd: shell.cwd,
|
|
214
|
+
sandboxed: shell.sandboxed === true,
|
|
215
|
+
sandbox_backend: shell.sandboxBackend || null,
|
|
216
|
+
sandbox_network_mode: shell.sandboxNetworkMode || null,
|
|
217
|
+
sandbox_dir: shell.sandboxDir || null,
|
|
218
|
+
sandbox_home_dir: shell.sandboxHomeDir || null,
|
|
219
|
+
sandbox_tmp_dir: shell.sandboxTmpDir || null,
|
|
220
|
+
sandbox_cache_dir: shell.sandboxCacheDir || null,
|
|
214
221
|
pid: typeof shell.pid === "number" ? shell.pid : null,
|
|
215
222
|
version: shell.version,
|
|
216
223
|
started_at: shell.startedAt,
|
|
@@ -255,6 +262,13 @@ export function flattenShellExecResponse(params: {
|
|
|
255
262
|
status: shell.status,
|
|
256
263
|
cmd: shell.cmd,
|
|
257
264
|
cwd: shell.cwd,
|
|
265
|
+
sandboxed: shell.sandboxed === true,
|
|
266
|
+
sandbox_backend: shell.sandboxBackend || null,
|
|
267
|
+
sandbox_network_mode: shell.sandboxNetworkMode || null,
|
|
268
|
+
sandbox_dir: shell.sandboxDir || null,
|
|
269
|
+
sandbox_home_dir: shell.sandboxHomeDir || null,
|
|
270
|
+
sandbox_tmp_dir: shell.sandboxTmpDir || null,
|
|
271
|
+
sandbox_cache_dir: shell.sandboxCacheDir || null,
|
|
258
272
|
exit_code: typeof shell.exitCode === "number" ? shell.exitCode : null,
|
|
259
273
|
output: chunk?.output || "",
|
|
260
274
|
original_chars: chunk?.originalChars ?? 0,
|
|
@@ -54,6 +54,14 @@ export type ShellSessionSnapshot = {
|
|
|
54
54
|
sandboxBackend?: string;
|
|
55
55
|
/** 当前 shell 采用的 sandbox 网络模式。 */
|
|
56
56
|
sandboxNetworkMode?: "off" | "restricted" | "full";
|
|
57
|
+
/** 当前 agent 级 sandbox 的持久目录。 */
|
|
58
|
+
sandboxDir?: string;
|
|
59
|
+
/** 当前 shell 在 sandbox 中使用的 HOME。 */
|
|
60
|
+
sandboxHomeDir?: string;
|
|
61
|
+
/** 当前 shell 在 sandbox 中使用的临时目录。 */
|
|
62
|
+
sandboxTmpDir?: string;
|
|
63
|
+
/** 当前 shell 在 sandbox 中使用的 XDG cache 目录。 */
|
|
64
|
+
sandboxCacheDir?: string;
|
|
57
65
|
/** 当前 shell 状态。 */
|
|
58
66
|
status: ShellSessionStatus;
|
|
59
67
|
/** 子进程 pid;若尚未创建成功则为空。 */
|
package/src/index.ts
CHANGED
|
@@ -229,13 +229,8 @@ export type {
|
|
|
229
229
|
ImagePluginContent,
|
|
230
230
|
ImagePluginFileContent,
|
|
231
231
|
ImagePluginInput,
|
|
232
|
-
ImagePluginJobCreateResult,
|
|
233
|
-
ImagePluginJobResult,
|
|
234
|
-
ImagePluginJobStatus,
|
|
235
|
-
ImagePluginJobStatusResult,
|
|
236
232
|
ImagePluginMessage,
|
|
237
233
|
ImagePluginOptions,
|
|
238
|
-
ImagePluginResult,
|
|
239
234
|
ImagePluginTextContent,
|
|
240
235
|
} from "./types/plugin/ImagePlugin.js";
|
|
241
236
|
|
|
@@ -2,19 +2,15 @@
|
|
|
2
2
|
* ImagePlugin:Agent 内置图片生成插件。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
5
|
+
* - 对 Agent 只暴露同步体验的 `generate` action。
|
|
6
|
+
* - City / provider 的图片能力通过单个 image 函数注入。
|
|
7
7
|
* - action 返回 AI SDK UIMessage,后续由 plugin tool bridge 抽取 file parts 写回 assistant 消息。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import crypto from "node:crypto";
|
|
11
10
|
import type { AgentContext } from "@/types/runtime/agent/AgentContext.js";
|
|
12
11
|
import type { JsonObject, JsonValue } from "@/types/common/Json.js";
|
|
13
12
|
import type {
|
|
14
13
|
ImagePluginInput,
|
|
15
|
-
ImagePluginJobCreateResult,
|
|
16
|
-
ImagePluginJobResult,
|
|
17
|
-
ImagePluginJobStatusResult,
|
|
18
14
|
ImagePluginOptions,
|
|
19
15
|
ImagePluginResult,
|
|
20
16
|
} from "@/types/plugin/ImagePlugin.js";
|
|
@@ -24,39 +20,6 @@ const DEFAULT_IMAGE_PLUGIN_NAME = "image";
|
|
|
24
20
|
const DEFAULT_IMAGE_PLUGIN_TITLE = "Image";
|
|
25
21
|
const DEFAULT_IMAGE_PLUGIN_DESCRIPTION =
|
|
26
22
|
"Generate images and return them as assistant file parts.";
|
|
27
|
-
const DEFAULT_WAIT_TIMEOUT_MS = 60_000;
|
|
28
|
-
const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
29
|
-
|
|
30
|
-
type LocalImageJobRecord = {
|
|
31
|
-
/**
|
|
32
|
-
* 图片任务唯一 ID。
|
|
33
|
-
*/
|
|
34
|
-
job_id: string;
|
|
35
|
-
/**
|
|
36
|
-
* 当前任务状态。
|
|
37
|
-
*/
|
|
38
|
-
status: "queued" | "running" | "succeeded" | "failed";
|
|
39
|
-
/**
|
|
40
|
-
* 成功时的图片结果。
|
|
41
|
-
*/
|
|
42
|
-
result?: ImagePluginResult;
|
|
43
|
-
/**
|
|
44
|
-
* 失败时的错误信息。
|
|
45
|
-
*/
|
|
46
|
-
error?: string;
|
|
47
|
-
/**
|
|
48
|
-
* 人类可读状态说明。
|
|
49
|
-
*/
|
|
50
|
-
message?: string;
|
|
51
|
-
/**
|
|
52
|
-
* 任务创建时间。
|
|
53
|
-
*/
|
|
54
|
-
created_at: string;
|
|
55
|
-
/**
|
|
56
|
-
* 任务更新时间。
|
|
57
|
-
*/
|
|
58
|
-
updated_at: string;
|
|
59
|
-
};
|
|
60
23
|
|
|
61
24
|
/**
|
|
62
25
|
* 判断值是否为普通对象。
|
|
@@ -77,45 +40,17 @@ function normalize_image_payload(payload: JsonValue | undefined): ImagePluginInp
|
|
|
77
40
|
return { ...record } as ImagePluginInput;
|
|
78
41
|
}
|
|
79
42
|
|
|
80
|
-
function normalize_job_id_payload(payload: JsonValue | undefined): { job_id: string } {
|
|
81
|
-
const record = to_record(payload ?? {});
|
|
82
|
-
const job_id = String(record?.job_id || "").trim();
|
|
83
|
-
if (!job_id) {
|
|
84
|
-
throw new TypeError("ImagePlugin job action requires job_id");
|
|
85
|
-
}
|
|
86
|
-
return { job_id };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
43
|
/**
|
|
90
44
|
* 校验 image 函数返回的 UIMessage。
|
|
91
45
|
*/
|
|
92
46
|
function normalize_image_result(result: ImagePluginResult): ImagePluginResult {
|
|
93
47
|
const record = to_record(result);
|
|
94
48
|
if (!record || !Array.isArray(record.parts)) {
|
|
95
|
-
throw new TypeError("ImagePlugin image
|
|
49
|
+
throw new TypeError("ImagePlugin image provider must return an AI SDK UIMessage");
|
|
96
50
|
}
|
|
97
51
|
return result;
|
|
98
52
|
}
|
|
99
53
|
|
|
100
|
-
/**
|
|
101
|
-
* 归一化任务状态查询结果,确保 status action 不携带图片结果。
|
|
102
|
-
*/
|
|
103
|
-
function normalize_job_status_result(
|
|
104
|
-
result: ImagePluginJobStatusResult,
|
|
105
|
-
): ImagePluginJobStatusResult {
|
|
106
|
-
return {
|
|
107
|
-
job_id: result.job_id,
|
|
108
|
-
status: result.status,
|
|
109
|
-
...(result.message ? { message: result.message } : {}),
|
|
110
|
-
...(result.error ? { error: result.error } : {}),
|
|
111
|
-
...(typeof result.poll_after_ms === "number"
|
|
112
|
-
? { poll_after_ms: result.poll_after_ms }
|
|
113
|
-
: {}),
|
|
114
|
-
...(result.created_at ? { created_at: result.created_at } : {}),
|
|
115
|
-
...(result.updated_at ? { updated_at: result.updated_at } : {}),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
54
|
/**
|
|
120
55
|
* Agent 图片生成插件。
|
|
121
56
|
*/
|
|
@@ -135,13 +70,7 @@ export class ImagePlugin extends BasePlugin {
|
|
|
135
70
|
*/
|
|
136
71
|
readonly description: string;
|
|
137
72
|
|
|
138
|
-
private readonly image: ImagePluginOptions["image"]
|
|
139
|
-
private readonly create_job?: ImagePluginOptions["create"];
|
|
140
|
-
private readonly read_job_status?: ImagePluginOptions["status"];
|
|
141
|
-
private readonly read_job_result?: ImagePluginOptions["result"];
|
|
142
|
-
private readonly wait_timeout_ms: number;
|
|
143
|
-
private readonly poll_interval_ms: number;
|
|
144
|
-
private readonly local_jobs = new Map<string, LocalImageJobRecord>();
|
|
73
|
+
private readonly image: NonNullable<ImagePluginOptions["image"]>;
|
|
145
74
|
|
|
146
75
|
constructor(options: ImagePluginOptions) {
|
|
147
76
|
super();
|
|
@@ -149,23 +78,8 @@ export class ImagePlugin extends BasePlugin {
|
|
|
149
78
|
if (!name) {
|
|
150
79
|
throw new Error("ImagePlugin requires a non-empty name");
|
|
151
80
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
);
|
|
155
|
-
if (
|
|
156
|
-
has_custom_job_api &&
|
|
157
|
-
(typeof options.create !== "function" ||
|
|
158
|
-
typeof options.status !== "function" ||
|
|
159
|
-
typeof options.result !== "function")
|
|
160
|
-
) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
"ImagePlugin custom job API requires create, status, and result functions",
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
if (!has_custom_job_api && typeof options.image !== "function") {
|
|
166
|
-
throw new Error(
|
|
167
|
-
"ImagePlugin requires either image(input) or create/status/result functions",
|
|
168
|
-
);
|
|
81
|
+
if (typeof options.image !== "function") {
|
|
82
|
+
throw new Error("ImagePlugin requires an image function");
|
|
169
83
|
}
|
|
170
84
|
this.name = name;
|
|
171
85
|
this.title = String(options.title || DEFAULT_IMAGE_PLUGIN_TITLE).trim();
|
|
@@ -173,17 +87,6 @@ export class ImagePlugin extends BasePlugin {
|
|
|
173
87
|
options.description || DEFAULT_IMAGE_PLUGIN_DESCRIPTION,
|
|
174
88
|
).trim();
|
|
175
89
|
this.image = options.image;
|
|
176
|
-
this.create_job = options.create;
|
|
177
|
-
this.read_job_status = options.status;
|
|
178
|
-
this.read_job_result = options.result;
|
|
179
|
-
this.wait_timeout_ms =
|
|
180
|
-
typeof options.wait_timeout_ms === "number" && options.wait_timeout_ms > 0
|
|
181
|
-
? options.wait_timeout_ms
|
|
182
|
-
: DEFAULT_WAIT_TIMEOUT_MS;
|
|
183
|
-
this.poll_interval_ms =
|
|
184
|
-
typeof options.poll_interval_ms === "number" && options.poll_interval_ms > 0
|
|
185
|
-
? options.poll_interval_ms
|
|
186
|
-
: DEFAULT_POLL_INTERVAL_MS;
|
|
187
90
|
}
|
|
188
91
|
|
|
189
92
|
/**
|
|
@@ -191,202 +94,26 @@ export class ImagePlugin extends BasePlugin {
|
|
|
191
94
|
*/
|
|
192
95
|
system(_context: AgentContext): string {
|
|
193
96
|
return [
|
|
194
|
-
"Image generation is available through the plugin_call tool
|
|
195
|
-
`Call plugin "${this.name}" action "
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"Use action \"generate\" only as a compatibility shortcut when you explicitly need to wait for completion.",
|
|
199
|
-
"Pass a JSON payload with prompt, optional size/aspect_ratio/quality/n, and optional provider_options to create/generate.",
|
|
97
|
+
"Image generation is available through the plugin_call tool.",
|
|
98
|
+
`Call plugin "${this.name}" action "generate" when the user asks to create, render, draw, or edit an image.`,
|
|
99
|
+
"Pass a JSON payload with prompt, optional size/aspect_ratio/quality/n, and optional provider_options.",
|
|
100
|
+
"The generated image files will be attached to the final assistant message automatically.",
|
|
200
101
|
].join("\n");
|
|
201
102
|
}
|
|
202
103
|
|
|
203
|
-
private
|
|
204
|
-
|
|
205
|
-
throw new Error("ImagePlugin local image job requires image(input)");
|
|
206
|
-
}
|
|
207
|
-
const now = new Date().toISOString();
|
|
208
|
-
const job_id = `img_${crypto.randomUUID()}`;
|
|
209
|
-
const record: LocalImageJobRecord = {
|
|
210
|
-
job_id,
|
|
211
|
-
status: "running",
|
|
212
|
-
message: "image job is running",
|
|
213
|
-
created_at: now,
|
|
214
|
-
updated_at: now,
|
|
215
|
-
};
|
|
216
|
-
this.local_jobs.set(job_id, record);
|
|
217
|
-
|
|
218
|
-
void this.run_local_job(record, input, this.image);
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
job_id,
|
|
222
|
-
status: record.status,
|
|
223
|
-
message: record.message,
|
|
224
|
-
poll_after_ms: this.poll_interval_ms,
|
|
225
|
-
created_at: record.created_at,
|
|
226
|
-
updated_at: record.updated_at,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private async run_local_job(
|
|
231
|
-
record: LocalImageJobRecord,
|
|
232
|
-
input: ImagePluginInput,
|
|
233
|
-
image: NonNullable<ImagePluginOptions["image"]>,
|
|
234
|
-
): Promise<void> {
|
|
235
|
-
try {
|
|
236
|
-
const message = normalize_image_result(await image(input));
|
|
237
|
-
record.status = "succeeded";
|
|
238
|
-
record.result = message;
|
|
239
|
-
record.message = "image job succeeded";
|
|
240
|
-
record.updated_at = new Date().toISOString();
|
|
241
|
-
} catch (error) {
|
|
242
|
-
record.status = "failed";
|
|
243
|
-
record.error = String(error);
|
|
244
|
-
record.message = "image job failed";
|
|
245
|
-
record.updated_at = new Date().toISOString();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private read_local_job(job_id: string): LocalImageJobRecord {
|
|
250
|
-
const record = this.local_jobs.get(job_id);
|
|
251
|
-
if (!record) {
|
|
252
|
-
throw new Error(`Unknown image job: ${job_id}`);
|
|
253
|
-
}
|
|
254
|
-
return record;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private serialize_local_status(record: LocalImageJobRecord): ImagePluginJobStatusResult {
|
|
258
|
-
return {
|
|
259
|
-
job_id: record.job_id,
|
|
260
|
-
status: record.status,
|
|
261
|
-
...(record.message ? { message: record.message } : {}),
|
|
262
|
-
...(record.error ? { error: record.error } : {}),
|
|
263
|
-
...(record.status === "running" || record.status === "queued"
|
|
264
|
-
? { poll_after_ms: this.poll_interval_ms }
|
|
265
|
-
: {}),
|
|
266
|
-
created_at: record.created_at,
|
|
267
|
-
updated_at: record.updated_at,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
private serialize_local_result(record: LocalImageJobRecord): ImagePluginJobResult {
|
|
272
|
-
return {
|
|
273
|
-
job_id: record.job_id,
|
|
274
|
-
status: record.status,
|
|
275
|
-
...(record.result ? { result: record.result } : {}),
|
|
276
|
-
...(record.error ? { error: record.error } : {}),
|
|
277
|
-
...(record.message ? { message: record.message } : {}),
|
|
278
|
-
created_at: record.created_at,
|
|
279
|
-
updated_at: record.updated_at,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private async wait_for_job(job_id: string): Promise<ImagePluginResult> {
|
|
284
|
-
const deadline = Date.now() + this.wait_timeout_ms;
|
|
285
|
-
while (Date.now() <= deadline) {
|
|
286
|
-
const result = this.read_job_result
|
|
287
|
-
? await this.read_job_result({ job_id })
|
|
288
|
-
: this.serialize_local_result(this.read_local_job(job_id));
|
|
289
|
-
if (result.status === "succeeded" && result.result) {
|
|
290
|
-
return normalize_image_result(result.result);
|
|
291
|
-
}
|
|
292
|
-
if (result.status === "failed") {
|
|
293
|
-
throw new Error(result.error || result.message || "image job failed");
|
|
294
|
-
}
|
|
295
|
-
await new Promise((resolve) => setTimeout(resolve, this.poll_interval_ms));
|
|
296
|
-
}
|
|
297
|
-
throw new Error(`image job timed out: ${job_id}`);
|
|
104
|
+
private async generate_image(input: ImagePluginInput): Promise<ImagePluginResult> {
|
|
105
|
+
return normalize_image_result(await this.image(input));
|
|
298
106
|
}
|
|
299
107
|
|
|
300
108
|
/**
|
|
301
109
|
* 显式 action 集合。
|
|
302
110
|
*/
|
|
303
111
|
readonly actions = {
|
|
304
|
-
create: {
|
|
305
|
-
execute: async ({ payload }: { payload: JsonValue }) => {
|
|
306
|
-
try {
|
|
307
|
-
const input = normalize_image_payload(payload);
|
|
308
|
-
const result = this.create_job
|
|
309
|
-
? await this.create_job(input)
|
|
310
|
-
: this.create_local_job(input);
|
|
311
|
-
return {
|
|
312
|
-
success: true,
|
|
313
|
-
data: result as unknown as JsonObject,
|
|
314
|
-
message: result.message || "image job created",
|
|
315
|
-
};
|
|
316
|
-
} catch (error) {
|
|
317
|
-
return {
|
|
318
|
-
success: false,
|
|
319
|
-
error: String(error),
|
|
320
|
-
message: String(error),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
status: {
|
|
326
|
-
execute: async ({ payload }: { payload: JsonValue }) => {
|
|
327
|
-
try {
|
|
328
|
-
const input = normalize_job_id_payload(payload);
|
|
329
|
-
const result = this.read_job_status
|
|
330
|
-
? normalize_job_status_result(await this.read_job_status(input))
|
|
331
|
-
: this.serialize_local_status(this.read_local_job(input.job_id));
|
|
332
|
-
return {
|
|
333
|
-
success: true,
|
|
334
|
-
data: result as unknown as JsonObject,
|
|
335
|
-
message: result.message || `image job ${result.status}`,
|
|
336
|
-
};
|
|
337
|
-
} catch (error) {
|
|
338
|
-
return {
|
|
339
|
-
success: false,
|
|
340
|
-
error: String(error),
|
|
341
|
-
message: String(error),
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
result: {
|
|
347
|
-
execute: async ({ payload }: { payload: JsonValue }) => {
|
|
348
|
-
try {
|
|
349
|
-
const input = normalize_job_id_payload(payload);
|
|
350
|
-
const result = this.read_job_result
|
|
351
|
-
? await this.read_job_result(input)
|
|
352
|
-
: this.serialize_local_result(this.read_local_job(input.job_id));
|
|
353
|
-
if (result.status === "succeeded" && result.result) {
|
|
354
|
-
return {
|
|
355
|
-
success: true,
|
|
356
|
-
data: result.result as unknown as JsonObject,
|
|
357
|
-
message: result.message || "image job succeeded",
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
if (result.status === "failed") {
|
|
361
|
-
return {
|
|
362
|
-
success: false,
|
|
363
|
-
data: result as unknown as JsonObject,
|
|
364
|
-
error: result.error || result.message || "image job failed",
|
|
365
|
-
message: result.message || "image job failed",
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
return {
|
|
369
|
-
success: true,
|
|
370
|
-
data: result as unknown as JsonObject,
|
|
371
|
-
message: result.message || `image job ${result.status}`,
|
|
372
|
-
};
|
|
373
|
-
} catch (error) {
|
|
374
|
-
return {
|
|
375
|
-
success: false,
|
|
376
|
-
error: String(error),
|
|
377
|
-
message: String(error),
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
},
|
|
382
112
|
generate: {
|
|
383
113
|
execute: async ({ payload }: { payload: JsonValue }) => {
|
|
384
114
|
try {
|
|
385
115
|
const input = normalize_image_payload(payload);
|
|
386
|
-
const
|
|
387
|
-
? await this.create_job(input)
|
|
388
|
-
: this.create_local_job(input);
|
|
389
|
-
const message = await this.wait_for_job(job.job_id);
|
|
116
|
+
const message = await this.generate_image(input);
|
|
390
117
|
return {
|
|
391
118
|
success: true,
|
|
392
119
|
data: message as unknown as JsonObject,
|