@dai_ming/plugin-deliverables 1.0.9 → 1.0.14
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/README.md +19 -6
- package/agents-rules/deliverables.md +19 -27
- package/index.js +217 -0
- package/install.js +123 -27
- package/mcp-servers/deliverables.js +92 -20
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +11 -0
- package/package.json +10 -2
- package/skills/deliverables/SKILL.md +12 -10
package/README.md
CHANGED
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则和 `openclaw.json` 配置一次性落到运行目录里,让 Agent 默认把生成文件上传成可访问的交付物链接。
|
|
4
4
|
|
|
5
|
+
当前版本增强点:
|
|
6
|
+
|
|
7
|
+
- 上传成功后返回标准 Markdown 链接块;同时通过运行时补丁把 palz-connector 中的“简介 + 链接”交付物回复拆成两条独立消息
|
|
8
|
+
- 单文件交付物会尽量保留原始文件后缀;如果模型漏掉后缀,插件会按内容类型自动补齐(如 `.md` / `.html`)
|
|
9
|
+
|
|
5
10
|
## 包内文件
|
|
6
11
|
|
|
7
12
|
| 文件 | 作用 |
|
|
8
13
|
|------|------|
|
|
9
|
-
| `install.js` | 安装器:负责复制插件、注册 MCP、安装 skill、注入 AGENTS 规则、补齐 `plugins.allow`、清 session |
|
|
14
|
+
| `install.js` | 安装器:负责复制插件、注册 MCP、安装 skill、注入 AGENTS 规则、补齐 `plugins.allow`、清 session,并刷新旧的 deliverables MCP 进程 |
|
|
10
15
|
| `mcp-servers/deliverables.js` | MCP Server,暴露 `upload_deliverable` |
|
|
11
16
|
| `skills/deliverables/SKILL.md` | 约束模型优先走交付物上传,并统一写到 `output/` |
|
|
12
17
|
| `agents-rules/deliverables.md` | 注入到 `AGENTS.md` 的强约束规则 |
|
|
13
|
-
| `openclaw-plugin.json` |
|
|
18
|
+
| `openclaw-plugin.json` | 兼容当前安装脚本的清单,声明 MCP、skill、rules 和运行时配置 |
|
|
19
|
+
| `openclaw.plugin.json` | OpenClaw 运行时插件清单,用于真正加载 `index.js` 扩展 |
|
|
14
20
|
|
|
15
21
|
## 推荐安装方式
|
|
16
22
|
|
|
@@ -20,7 +26,7 @@ OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则
|
|
|
20
26
|
|
|
21
27
|
```yaml
|
|
22
28
|
installPlugins:
|
|
23
|
-
- "@dai_ming/plugin-deliverables@1.0.
|
|
29
|
+
- "@dai_ming/plugin-deliverables@1.0.14"
|
|
24
30
|
```
|
|
25
31
|
|
|
26
32
|
现有 chart 会在 init 阶段完成安装。
|
|
@@ -31,7 +37,7 @@ installPlugins:
|
|
|
31
37
|
|
|
32
38
|
```bash
|
|
33
39
|
npm config set registry https://registry.npmmirror.com
|
|
34
|
-
npm install @dai_ming/plugin-deliverables@1.0.
|
|
40
|
+
npm install @dai_ming/plugin-deliverables@1.0.14
|
|
35
41
|
node node_modules/@dai_ming/plugin-deliverables/install.js
|
|
36
42
|
```
|
|
37
43
|
|
|
@@ -57,9 +63,16 @@ node node_modules/@dai_ming/plugin-deliverables/install.js \
|
|
|
57
63
|
- 注册 `mcp.servers.deliverables`
|
|
58
64
|
- 启用 `skills.entries.deliverables`
|
|
59
65
|
- 启用 `plugins.entries.plugin-deliverables`
|
|
66
|
+
- 把 `~/.openclaw/extensions-extra/plugin-deliverables` 写入 `plugins.load.paths`
|
|
60
67
|
- 把 `plugin-deliverables` 加入 `plugins.allow`
|
|
61
68
|
7. 清理 session,让下一条消息重新读取 prompt / skill / tool 配置
|
|
62
69
|
8. 如果 agent 或全局存在 `tools.allow` 白名单,自动补上 `deliverables__upload_deliverable`
|
|
70
|
+
9. 如果当前 pod 里已经跑着旧版 `deliverables.js` MCP 进程,会自动结束旧进程,让它按新文件重新拉起,避免“磁盘已更新但 toolResult 还是旧格式”
|
|
71
|
+
|
|
72
|
+
补充说明:
|
|
73
|
+
|
|
74
|
+
- 当前 OpenClaw 运行时默认只扫描 `~/.openclaw/extensions`;`extensions-extra` 里的 JS 插件只有进入 `plugins.load.paths` 才会被当成可执行 `openclaw` 插件加载
|
|
75
|
+
- 安装脚本会先把插件源码暂存到系统临时目录再复制,避免在 `extensions-extra` 原地执行安装时把源目录自己删掉
|
|
63
76
|
|
|
64
77
|
## 是否需要重启 Pod
|
|
65
78
|
|
|
@@ -67,7 +80,7 @@ node node_modules/@dai_ming/plugin-deliverables/install.js \
|
|
|
67
80
|
|
|
68
81
|
原因:
|
|
69
82
|
- 安装脚本会直接改 `~/.openclaw/openclaw.json`
|
|
70
|
-
- `plugins.allow` 变化会触发 OpenClaw 的 watcher,进程级重载大约 15 秒
|
|
83
|
+
- `plugins.allow` / `plugins.load.paths` 变化会触发 OpenClaw 的 watcher,进程级重载大约 15 秒
|
|
71
84
|
- session 也会被清掉,所以下一条消息会重新读取最新的 `AGENTS.md` / skill
|
|
72
85
|
|
|
73
86
|
建议做法:
|
|
@@ -136,4 +149,4 @@ curl -H "X-API-Key: $CLAW_GATEWAY_API_KEY" "$CLAW_GATEWAY_URL/healthz"
|
|
|
136
149
|
|
|
137
150
|
### 返回的是 `/preview/:uuid` 或内部地址
|
|
138
151
|
|
|
139
|
-
`1.0.
|
|
152
|
+
`1.0.14` 起,安装脚本会在落盘新的 `deliverables.js` 后自动刷新正在运行的旧 MCP 进程,避免出现“插件目录和 `extensions-extra` 都是新代码,但 `~/.openclaw/mcp-servers/deliverables.js` 对应的常驻进程仍返回旧 `reply_markdown`”的问题。此前 `1.0.13` 已经完成 `output` 直链修正、后缀保留,以及 palz-connector 中“内容简介 + 链接”自动拆成两条消息,第二条只保留一个最终 HTTPS 链接。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- DELIVERABLE_LINK_RULES_START -->
|
|
2
2
|
## Deliverables -- URL Echo Rule (HARD CONSTRAINT)
|
|
3
3
|
|
|
4
|
-
When you call the deliverables upload tool,
|
|
4
|
+
When you call the deliverables upload tool, the user-facing output MUST make the deliverable easy to open and easy to understand.
|
|
5
5
|
|
|
6
6
|
Tool name note:
|
|
7
7
|
|
|
@@ -20,28 +20,20 @@ Tool name note:
|
|
|
20
20
|
- 3-6 bullet points describing the actual sections, highlights, features, or focus of the deliverable
|
|
21
21
|
3. The intro should be noticeably more informative than one sentence. For normal articles/reports/introductions, aim for roughly 80-200 Chinese characters total before the links.
|
|
22
22
|
4. The bullet points must describe the real deliverable content. Do not use empty filler like "内容丰富" / "结构清晰" / "值得阅读" unless paired with concrete details.
|
|
23
|
-
5. If tool result contains `reply_markdown`, append a `### 访问链接` heading and then append that field verbatim on the following
|
|
24
|
-
6.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`文件列表:[查看目录](<full_url>)`
|
|
30
|
-
then keep that exact label and URL. Do not rewrite it back to a raw long link.
|
|
31
|
-
9. Keep URL target value exactly from tool result (no shortening, no masking, no redirect rewrite).
|
|
32
|
-
10. Use short link labels (`点击预览` / `点击下载` / `查看目录`) to avoid exposing long raw URLs.
|
|
33
|
-
11. Do not say "已上传/已完成" without links.
|
|
34
|
-
12. Do not output naked long URLs outside Markdown link syntax.
|
|
35
|
-
13. Keep the intro concise but substantive; do not paste the full file content, saved-path text, or a huge essay after the Markdown links.
|
|
23
|
+
5. If tool result contains `reply_markdown`, append a `### 访问链接` heading and then append that field verbatim on the following line. Do not rewrite the URL inside it.
|
|
24
|
+
6. Otherwise, if tool result has `primary_url`, output only that one full URL on its own line after `### 访问链接`.
|
|
25
|
+
7. If `primary_url` is missing, fall back to exactly one link: prefer `preview_url`, otherwise `download_url`.
|
|
26
|
+
8. Keep URL target value exactly from tool result (no shortening, no masking, no redirect rewrite).
|
|
27
|
+
9. Only output one final deliverable URL. Do not output both preview and download.
|
|
28
|
+
10. Keep the intro concise but substantive; do not paste the full file content, saved-path text, or a huge essay after the URL.
|
|
36
29
|
|
|
37
30
|
### Forbidden output behavior
|
|
38
31
|
|
|
39
32
|
- "可点击预览链接查看" but no actual URL
|
|
40
33
|
- A single short sentence plus links when the deliverable is an article/report/introduction and concrete highlights are available
|
|
41
34
|
- Replacing URL target with a non-original URL
|
|
42
|
-
- Omitting
|
|
43
|
-
- Outputting
|
|
44
|
-
- Replacing `文件列表:[查看目录](...)` with a naked URL or a zip description
|
|
35
|
+
- Omitting the final URL when the tool already returned one
|
|
36
|
+
- Outputting both preview and download when one shareable URL is enough
|
|
45
37
|
- Using a generic boilerplate intro that does not mention the actual deliverable content
|
|
46
38
|
<!-- DELIVERABLE_LINK_RULES_END -->
|
|
47
39
|
|
|
@@ -61,8 +53,12 @@ Tool name note:
|
|
|
61
53
|
1. Create content under current agent workspace `output/` directory first (for example `output/report.md`), then call the deliverables upload tool exposed in this session.
|
|
62
54
|
2. If you need to use the `write` tool, the target path MUST be inside `output/`. Never write deliverable files to the workspace root.
|
|
63
55
|
3. If you already wrote the file to the wrong place, move/copy it into `output/` before finalizing the task and before describing the saved path to the user.
|
|
64
|
-
4.
|
|
65
|
-
|
|
56
|
+
4. The deliverable file name MUST preserve the intended extension exactly. Examples:
|
|
57
|
+
- Markdown article: `刘德华介绍.md`
|
|
58
|
+
- Markdown note: `周杰伦.md`
|
|
59
|
+
- HTML page: `996专题.html`
|
|
60
|
+
- Text file: `旅行清单.txt`
|
|
61
|
+
5. Never drop the suffix from the final deliverable file name. If the user asked for Markdown, keep `.md`; if HTML, keep `.html`.
|
|
66
62
|
6. For multi-file deliverables (game/site), you MUST prefer `type=game` with `files[]`, keep files as a folder structure, and do NOT zip before upload unless the user explicitly asks for a zip package.
|
|
67
63
|
7. Static multi-file game/site deliverables SHOULD include a root `index.html` so the preview link can open the homepage directly.
|
|
68
64
|
8. If a generated project requires starting a separate backend service, custom port, database, or long-running process to work, do NOT pretend the deliverables preview can run it. Tell the user that deliverables preview only supports static output, and that runtime projects need deployment/ingress instead.
|
|
@@ -73,15 +69,11 @@ Tool name note:
|
|
|
73
69
|
- `### 内容概览`
|
|
74
70
|
- 3-6 bullet points summarizing the actual content sections, highlights, or key features
|
|
75
71
|
12. For ordinary articles/reports/introductions, the intro before the links should normally reach roughly 80-200 Chinese characters total.
|
|
76
|
-
13. If tool result contains `reply_markdown`, add `### 访问链接` and append that field verbatim on the following
|
|
77
|
-
14. Otherwise final reply MUST include
|
|
78
|
-
|
|
79
|
-
`下载链接:[点击下载](<full_url>)`
|
|
80
|
-
15. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
|
|
81
|
-
`文件列表:[查看目录](<full_url>)`
|
|
82
|
-
Keep that format instead of forcing a zip link.
|
|
72
|
+
13. If tool result contains `reply_markdown`, add `### 访问链接` and append that field verbatim on the following line.
|
|
73
|
+
14. Otherwise final reply MUST include exactly one full URL after `### 访问链接`: prefer `primary_url`, otherwise `preview_url`, otherwise `download_url`.
|
|
74
|
+
15. Do NOT output preview/download two条链接,也不要把目录链接再包装成 zip 说明。
|
|
83
75
|
16. Do NOT only say "已保存到工作空间".
|
|
84
|
-
17. Do NOT append workspace path or
|
|
76
|
+
17. Do NOT append workspace path or extra raw URL blocks after the final URL.
|
|
85
77
|
|
|
86
78
|
### Exception
|
|
87
79
|
|
package/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
|
|
5
|
+
|
|
6
|
+
function isString(value) {
|
|
7
|
+
return typeof value === "string";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function trimTrailingBlankLines(lines) {
|
|
11
|
+
var out = lines.slice();
|
|
12
|
+
while (out.length > 0 && /^\s*$/.test(out[out.length - 1])) {
|
|
13
|
+
out.pop();
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripBulletPrefix(line) {
|
|
19
|
+
return String(line || "").replace(/^\s*[-*+]\s+/, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isLinksHeading(line) {
|
|
23
|
+
return /^\s*#{1,6}\s*访问链接\s*$/u.test(String(line || ""));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractDeliverableURL(line) {
|
|
27
|
+
var text = String(line || "").trim();
|
|
28
|
+
if (!text) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
var markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
|
|
32
|
+
if (markdownLink && markdownLink[1]) {
|
|
33
|
+
return markdownLink[1].trim();
|
|
34
|
+
}
|
|
35
|
+
var rawLink = text.match(/https?:\/\/\S+/iu);
|
|
36
|
+
if (rawLink && rawLink[0]) {
|
|
37
|
+
return rawLink[0].trim();
|
|
38
|
+
}
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isDeliverableLinkLine(line) {
|
|
43
|
+
var text = String(line || "");
|
|
44
|
+
if (/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(text)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return !!extractDeliverableURL(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function splitDeliverableMessage(text) {
|
|
51
|
+
var normalized = String(text || "").replace(/\r\n/g, "\n").trim();
|
|
52
|
+
if (!normalized) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var lines = normalized.split("\n");
|
|
57
|
+
var firstLinkIndex = -1;
|
|
58
|
+
var i;
|
|
59
|
+
for (i = 0; i < lines.length; i += 1) {
|
|
60
|
+
if (isDeliverableLinkLine(lines[i])) {
|
|
61
|
+
firstLinkIndex = i;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (firstLinkIndex < 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var summaryLines = trimTrailingBlankLines(lines.slice(0, firstLinkIndex));
|
|
70
|
+
if (summaryLines.length > 0 && isLinksHeading(summaryLines[summaryLines.length - 1])) {
|
|
71
|
+
summaryLines.pop();
|
|
72
|
+
}
|
|
73
|
+
summaryLines = trimTrailingBlankLines(summaryLines);
|
|
74
|
+
|
|
75
|
+
var linkLines = [];
|
|
76
|
+
for (i = firstLinkIndex; i < lines.length; i += 1) {
|
|
77
|
+
var line = lines[i];
|
|
78
|
+
if (isLinksHeading(line)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (isDeliverableLinkLine(line)) {
|
|
82
|
+
var url = extractDeliverableURL(stripBulletPrefix(line));
|
|
83
|
+
if (url) {
|
|
84
|
+
linkLines.push(url);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var summary = summaryLines.join("\n").trim();
|
|
90
|
+
var links = linkLines.length > 0 ? linkLines[0] : "";
|
|
91
|
+
if (!summary || !links) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
summary: summary,
|
|
97
|
+
links: links,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function cloneBody(body, content, suffix) {
|
|
102
|
+
var next = {};
|
|
103
|
+
Object.keys(body).forEach(function(key) {
|
|
104
|
+
next[key] = body[key];
|
|
105
|
+
});
|
|
106
|
+
next.content = content;
|
|
107
|
+
if (isString(body.msg_id) && body.msg_id.trim()) {
|
|
108
|
+
next.msg_id = body.msg_id + suffix;
|
|
109
|
+
}
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function shouldSplitPalzPayload(body) {
|
|
114
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (!isString(body.content)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (body.stream_id || body.seq !== undefined || body.delta !== undefined || body.is_final !== undefined) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (body.palz_msg_type || body.tool_content) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return splitDeliverableMessage(body.content);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveFetchURL(input) {
|
|
130
|
+
if (isString(input)) {
|
|
131
|
+
return input;
|
|
132
|
+
}
|
|
133
|
+
if (input && isString(input.url)) {
|
|
134
|
+
return input.url;
|
|
135
|
+
}
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveFetchMethod(input, init) {
|
|
140
|
+
if (init && isString(init.method) && init.method.trim()) {
|
|
141
|
+
return init.method.trim().toUpperCase();
|
|
142
|
+
}
|
|
143
|
+
if (input && !isString(input) && isString(input.method) && input.method.trim()) {
|
|
144
|
+
return input.method.trim().toUpperCase();
|
|
145
|
+
}
|
|
146
|
+
return "GET";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function installPalzFetchPatch(api) {
|
|
150
|
+
if (globalThis[FETCH_PATCH_KEY] && globalThis[FETCH_PATCH_KEY].installed) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
var originalFetch = globalThis.fetch;
|
|
154
|
+
if (typeof originalFetch !== "function") {
|
|
155
|
+
api.logger.warn && api.logger.warn("[plugin-deliverables] global fetch is unavailable; split patch skipped");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
var patchedFetch = async function(input, init) {
|
|
160
|
+
var url = resolveFetchURL(input);
|
|
161
|
+
var method = resolveFetchMethod(input, init);
|
|
162
|
+
if (method !== "POST" || !/\/bot\/send(?:\?|$)/.test(url)) {
|
|
163
|
+
return originalFetch(input, init);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var requestBody = init && init.body;
|
|
167
|
+
if (!isString(requestBody)) {
|
|
168
|
+
return originalFetch(input, init);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
var parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(requestBody);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return originalFetch(input, init);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var split = shouldSplitPalzPayload(parsed);
|
|
179
|
+
if (!split) {
|
|
180
|
+
return originalFetch(input, init);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
api.logger.info &&
|
|
184
|
+
api.logger.info(
|
|
185
|
+
"[plugin-deliverables] split deliverable reply for palz target=" +
|
|
186
|
+
String(parsed.conversation_id || ""),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
var baseInit = Object.assign({}, init);
|
|
190
|
+
var summaryInit = Object.assign({}, baseInit, {
|
|
191
|
+
body: JSON.stringify(cloneBody(parsed, split.summary, "__summary")),
|
|
192
|
+
});
|
|
193
|
+
var linksInit = Object.assign({}, baseInit, {
|
|
194
|
+
body: JSON.stringify(cloneBody(parsed, split.links, "__links")),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
var firstResponse = await originalFetch(input, summaryInit);
|
|
198
|
+
if (!firstResponse || !firstResponse.ok) {
|
|
199
|
+
return firstResponse;
|
|
200
|
+
}
|
|
201
|
+
return originalFetch(input, linksInit);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
globalThis.fetch = patchedFetch;
|
|
205
|
+
globalThis[FETCH_PATCH_KEY] = {
|
|
206
|
+
installed: true,
|
|
207
|
+
originalFetch: originalFetch,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function register(api) {
|
|
212
|
+
installPalzFetchPatch(api);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = register;
|
|
216
|
+
module.exports.default = register;
|
|
217
|
+
module.exports.id = "plugin-deliverables";
|
package/install.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
+
var childProcess = require("child_process");
|
|
4
5
|
var fs = require("fs");
|
|
5
6
|
var os = require("os");
|
|
6
7
|
var path = require("path");
|
|
@@ -134,6 +135,10 @@ function samePath(a, b) {
|
|
|
134
135
|
|
|
135
136
|
function copyRecursive(src, dst, dryRun) {
|
|
136
137
|
var stat = fs.lstatSync(src);
|
|
138
|
+
var baseName = path.basename(src);
|
|
139
|
+
if (baseName.indexOf("._") === 0 || /\.tgz$/i.test(baseName)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
137
142
|
if (stat.isSymbolicLink()) {
|
|
138
143
|
var target = fs.realpathSync(src);
|
|
139
144
|
copyRecursive(target, dst, dryRun);
|
|
@@ -142,7 +147,7 @@ function copyRecursive(src, dst, dryRun) {
|
|
|
142
147
|
if (stat.isDirectory()) {
|
|
143
148
|
ensureDir(dst, dryRun);
|
|
144
149
|
fs.readdirSync(src).forEach(function(entry) {
|
|
145
|
-
if (entry === "node_modules" || entry === ".git") return;
|
|
150
|
+
if (entry === "node_modules" || entry === ".git" || entry.indexOf("._") === 0 || /\.tgz$/i.test(entry)) return;
|
|
146
151
|
copyRecursive(path.join(src, entry), path.join(dst, entry), dryRun);
|
|
147
152
|
});
|
|
148
153
|
return;
|
|
@@ -163,6 +168,25 @@ function replaceDirectory(src, dst, dryRun) {
|
|
|
163
168
|
return true;
|
|
164
169
|
}
|
|
165
170
|
|
|
171
|
+
function stagePluginRoot(pluginRoot, dryRun) {
|
|
172
|
+
if (dryRun) {
|
|
173
|
+
return {
|
|
174
|
+
path: pluginRoot,
|
|
175
|
+
cleanup: function() {}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
var stageDir = fs.mkdtempSync(path.join(os.tmpdir(), "plugin-deliverables-"));
|
|
179
|
+
copyRecursive(pluginRoot, stageDir, false);
|
|
180
|
+
return {
|
|
181
|
+
path: stageDir,
|
|
182
|
+
cleanup: function() {
|
|
183
|
+
try {
|
|
184
|
+
fs.rmSync(stageDir, { recursive: true, force: true });
|
|
185
|
+
} catch (err) {}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
166
190
|
function normalizePluginEntryMap(entriesCfg, pluginId) {
|
|
167
191
|
if (!isPlainObject(entriesCfg)) return {};
|
|
168
192
|
var merged = {};
|
|
@@ -317,11 +341,25 @@ function applyPluginConfig(cfg, manifest, home) {
|
|
|
317
341
|
if (!isPlainObject(cfg)) cfg = {};
|
|
318
342
|
var pluginId = normalizeRuntimeName(manifest.name || "plugin-deliverables");
|
|
319
343
|
var mcpRoot = path.join(home, "mcp-servers");
|
|
344
|
+
var extensionsExtraDir = path.join(home, "extensions-extra", pluginId);
|
|
320
345
|
|
|
321
346
|
cfg.plugins = isPlainObject(cfg.plugins) ? cfg.plugins : {};
|
|
322
347
|
cfg.plugins.allow = Array.isArray(cfg.plugins.allow) ? cfg.plugins.allow.slice() : [];
|
|
323
348
|
if (cfg.plugins.allow.indexOf(pluginId) === -1) cfg.plugins.allow.push(pluginId);
|
|
324
349
|
cfg.plugins.entries = isPlainObject(cfg.plugins.entries) ? cfg.plugins.entries : {};
|
|
350
|
+
cfg.plugins.load = isPlainObject(cfg.plugins.load) ? cfg.plugins.load : {};
|
|
351
|
+
var existingLoadPaths = Array.isArray(cfg.plugins.load.paths) ? cfg.plugins.load.paths.slice() : [];
|
|
352
|
+
var nextLoadPaths = [];
|
|
353
|
+
var seenLoadPaths = {};
|
|
354
|
+
[extensionsExtraDir].concat(existingLoadPaths).forEach(function(rawPath) {
|
|
355
|
+
var item = String(rawPath || "").trim();
|
|
356
|
+
if (!item) return;
|
|
357
|
+
var key = path.resolve(item);
|
|
358
|
+
if (seenLoadPaths[key]) return;
|
|
359
|
+
seenLoadPaths[key] = true;
|
|
360
|
+
nextLoadPaths.push(item);
|
|
361
|
+
});
|
|
362
|
+
cfg.plugins.load.paths = nextLoadPaths;
|
|
325
363
|
|
|
326
364
|
var manifestPluginCfg = isPlainObject(manifest.openclaw_config) ? resolveEnvDeep(manifest.openclaw_config) : {};
|
|
327
365
|
var desiredEntries = {};
|
|
@@ -392,6 +430,53 @@ function installMcpServers(pluginRoot, manifest, home, dryRun) {
|
|
|
392
430
|
return copied;
|
|
393
431
|
}
|
|
394
432
|
|
|
433
|
+
function restartMcpServers(manifest, home, dryRun) {
|
|
434
|
+
var targets = [];
|
|
435
|
+
if (!isPlainObject(manifest.mcp_servers)) return targets;
|
|
436
|
+
|
|
437
|
+
Object.keys(manifest.mcp_servers).forEach(function(serverName) {
|
|
438
|
+
var server = manifest.mcp_servers[serverName];
|
|
439
|
+
if (!isPlainObject(server) || typeof server.script !== "string" || !server.script.trim()) return;
|
|
440
|
+
targets.push(path.join(home, "mcp-servers", path.basename(server.script)));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (dryRun || targets.length === 0) return [];
|
|
444
|
+
|
|
445
|
+
var raw = "";
|
|
446
|
+
try {
|
|
447
|
+
raw = childProcess.execFileSync("ps", ["-eo", "pid=,args="], {
|
|
448
|
+
encoding: "utf8",
|
|
449
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
450
|
+
});
|
|
451
|
+
} catch (err) {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
var restarted = [];
|
|
456
|
+
raw.split(/\r?\n/).forEach(function(line) {
|
|
457
|
+
var match = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
458
|
+
if (!match) return;
|
|
459
|
+
var pid = parseInt(match[1], 10);
|
|
460
|
+
var args = match[2] || "";
|
|
461
|
+
if (!pid || pid === process.pid) return;
|
|
462
|
+
var matchedTarget = "";
|
|
463
|
+
targets.some(function(target) {
|
|
464
|
+
if (args.indexOf(target) >= 0) {
|
|
465
|
+
matchedTarget = target;
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
});
|
|
470
|
+
if (!matchedTarget) return;
|
|
471
|
+
try {
|
|
472
|
+
process.kill(pid, "SIGTERM");
|
|
473
|
+
restarted.push({ pid: pid, script: matchedTarget });
|
|
474
|
+
} catch (err) {}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return restarted;
|
|
478
|
+
}
|
|
479
|
+
|
|
395
480
|
function clearSessions(home, dryRun) {
|
|
396
481
|
var cleared = [];
|
|
397
482
|
var agentsRoot = path.join(home, "agents");
|
|
@@ -416,7 +501,10 @@ function main() {
|
|
|
416
501
|
var home = path.resolve(args.home);
|
|
417
502
|
var manifestPath = path.join(pluginRoot, "openclaw-plugin.json");
|
|
418
503
|
if (!fs.existsSync(manifestPath)) {
|
|
419
|
-
|
|
504
|
+
manifestPath = path.join(pluginRoot, "openclaw.plugin.json");
|
|
505
|
+
}
|
|
506
|
+
if (!fs.existsSync(manifestPath)) {
|
|
507
|
+
throw new Error("missing plugin manifest at " + path.join(pluginRoot, "openclaw-plugin.json"));
|
|
420
508
|
}
|
|
421
509
|
var manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
422
510
|
var pluginName = normalizeRuntimeName(manifest.name || "plugin-deliverables");
|
|
@@ -426,32 +514,40 @@ function main() {
|
|
|
426
514
|
var agentIDs = collectAgentIDs(home, currentConfig);
|
|
427
515
|
var extensionsDir = path.join(home, "extensions", pluginName);
|
|
428
516
|
var extensionsExtraDir = path.join(home, "extensions-extra", pluginName);
|
|
517
|
+
var staged = stagePluginRoot(pluginRoot, args.dryRun);
|
|
429
518
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
519
|
+
try {
|
|
520
|
+
ensureDir(home, args.dryRun);
|
|
521
|
+
replaceDirectory(staged.path, extensionsDir, args.dryRun);
|
|
522
|
+
replaceDirectory(staged.path, extensionsExtraDir, args.dryRun);
|
|
523
|
+
var mcpFiles = installMcpServers(staged.path, manifest, home, args.dryRun);
|
|
524
|
+
var restartedMcpServers = restartMcpServers(manifest, home, args.dryRun);
|
|
525
|
+
var skillFiles = installSkills(staged.path, manifest, home, workspaceRoots, agentIDs, args.dryRun);
|
|
526
|
+
var agentFiles = injectRules(staged.path, manifest, workspaceRoots, args.dryRun);
|
|
527
|
+
var nextConfig = applyPluginConfig(currentConfig, manifest, home);
|
|
528
|
+
writeJSON(openclawPath, nextConfig, args.dryRun);
|
|
529
|
+
var clearedSessions = args.clearSessions ? clearSessions(home, args.dryRun) : [];
|
|
530
|
+
|
|
531
|
+
var summary = {
|
|
532
|
+
plugin: pluginName,
|
|
533
|
+
version: manifest.version || "",
|
|
534
|
+
openclaw_home: home,
|
|
535
|
+
dry_run: args.dryRun,
|
|
536
|
+
copied_extensions: [extensionsDir, extensionsExtraDir],
|
|
537
|
+
mcp_servers: mcpFiles,
|
|
538
|
+
restarted_mcp_servers: restartedMcpServers,
|
|
539
|
+
skill_files: skillFiles.length,
|
|
540
|
+
agents_files: agentFiles,
|
|
541
|
+
cleared_sessions: clearedSessions.length,
|
|
542
|
+
openclaw_json: openclawPath,
|
|
543
|
+
load_paths: nextConfig.plugins && nextConfig.plugins.load ? nextConfig.plugins.load.paths : []
|
|
544
|
+
};
|
|
545
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
546
|
+
if (!args.dryRun) {
|
|
547
|
+
console.log("Install complete. Wait about 15s for OpenClaw to reload plugins.allow/plugins.load.paths, then send a new message to verify.");
|
|
548
|
+
}
|
|
549
|
+
} finally {
|
|
550
|
+
staged.cleanup();
|
|
455
551
|
}
|
|
456
552
|
}
|
|
457
553
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
var http = require("http");
|
|
16
16
|
var https = require("https");
|
|
17
|
+
var path = require("path");
|
|
17
18
|
|
|
18
19
|
var GATEWAY_URL = (process.env.CLAW_GATEWAY_URL || "http://claw-gateway:8080").replace(/\/$/, "");
|
|
19
20
|
var RAW_GATEWAY_PUBLIC = (process.env.CLAW_GATEWAY_PUBLIC_URL || GATEWAY_URL).replace(/\/$/, "");
|
|
@@ -27,7 +28,7 @@ var TOOL_DEFS = [
|
|
|
27
28
|
{
|
|
28
29
|
name: "upload_deliverable",
|
|
29
30
|
description: [
|
|
30
|
-
"将 AI
|
|
31
|
+
"将 AI 生成的内容(文章、游戏、图片等)上传为交付物,返回一个可直接分享的访问链接。",
|
|
31
32
|
"单文件交付物:提供 content_text 或 content_base64。",
|
|
32
33
|
"多文件交付物(网页游戏/静态站点等):必须优先提供 files 列表,每项包含 name(相对路径)和 content_text 或 content_base64,不要先打 zip。",
|
|
33
34
|
"静态多文件预览建议在根目录提供 index.html;需要单独启动端口/后端服务的项目不属于交付物预览范围,应走部署流程。",
|
|
@@ -55,7 +56,7 @@ var TOOL_DEFS = [
|
|
|
55
56
|
},
|
|
56
57
|
file_name: {
|
|
57
58
|
type: "string",
|
|
58
|
-
description: "
|
|
59
|
+
description: "用户可见的文件名,必须尽量保留原始扩展名,例如 刘德华介绍.md、report.html。多文件交付时这里应是目录名/项目名,不要写成 .zip,除非用户明确要求压缩包。"
|
|
59
60
|
},
|
|
60
61
|
content_text: {
|
|
61
62
|
type: "string",
|
|
@@ -195,6 +196,75 @@ function sanitizeFileName(v) {
|
|
|
195
196
|
return name;
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function fileExtension(name) {
|
|
200
|
+
return path.extname(String(name || ""));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function hasFileExtension(name) {
|
|
204
|
+
return !!fileExtension(name);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function looksLikeHTML(text) {
|
|
208
|
+
var s = String(text || "").trim().toLowerCase();
|
|
209
|
+
if (!s) return false;
|
|
210
|
+
return s.indexOf("<!doctype html") === 0 ||
|
|
211
|
+
s.indexOf("<html") === 0 ||
|
|
212
|
+
s.indexOf("<head") === 0 ||
|
|
213
|
+
s.indexOf("<body") === 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function looksLikeMarkdown(text) {
|
|
217
|
+
var s = String(text || "").trim();
|
|
218
|
+
if (!s) return false;
|
|
219
|
+
return /^#{1,6}\s+\S/m.test(s) ||
|
|
220
|
+
/^\s*[-*+]\s+\S/m.test(s) ||
|
|
221
|
+
/^\s*\d+\.\s+\S/m.test(s) ||
|
|
222
|
+
/\[[^\]]+\]\([^)]+\)/.test(s) ||
|
|
223
|
+
/```/.test(s);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function defaultExtensionForDeliverable(type, contentText, files) {
|
|
227
|
+
var kind = String(type || "").trim().toLowerCase();
|
|
228
|
+
if (files && files.length > 0) return "";
|
|
229
|
+
switch (kind) {
|
|
230
|
+
case "article":
|
|
231
|
+
if (looksLikeHTML(contentText)) return ".html";
|
|
232
|
+
return ".md";
|
|
233
|
+
case "game":
|
|
234
|
+
return ".html";
|
|
235
|
+
case "image":
|
|
236
|
+
return ".png";
|
|
237
|
+
case "video":
|
|
238
|
+
return ".mp4";
|
|
239
|
+
case "zip":
|
|
240
|
+
return ".zip";
|
|
241
|
+
case "ppt":
|
|
242
|
+
return ".pptx";
|
|
243
|
+
case "link":
|
|
244
|
+
return "";
|
|
245
|
+
default:
|
|
246
|
+
if (looksLikeHTML(contentText)) return ".html";
|
|
247
|
+
if (looksLikeMarkdown(contentText)) return ".md";
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalizeDeliverableFileName(fileName, type, contentText, files) {
|
|
253
|
+
if (String(type || "").trim().toLowerCase() === "link") {
|
|
254
|
+
return String(fileName || "").trim();
|
|
255
|
+
}
|
|
256
|
+
var normalized = sanitizeFileName(fileName);
|
|
257
|
+
if (files && files.length > 0) {
|
|
258
|
+
return normalized || "deliverable";
|
|
259
|
+
}
|
|
260
|
+
if (hasFileExtension(normalized)) {
|
|
261
|
+
return normalized;
|
|
262
|
+
}
|
|
263
|
+
var ext = defaultExtensionForDeliverable(type, contentText, files);
|
|
264
|
+
if (!normalized) normalized = "deliverable";
|
|
265
|
+
return normalized + ext;
|
|
266
|
+
}
|
|
267
|
+
|
|
198
268
|
function extractUserIDFromResource(resourceID) {
|
|
199
269
|
var trimmed = String(resourceID || "").trim();
|
|
200
270
|
var prefix = "user_";
|
|
@@ -340,26 +410,18 @@ function formatGatewayError(statusCode, message, traceID) {
|
|
|
340
410
|
return msg;
|
|
341
411
|
}
|
|
342
412
|
|
|
343
|
-
function
|
|
413
|
+
function resolvePrimaryURL(opts) {
|
|
344
414
|
var previewURL = opts.previewURL || "";
|
|
345
415
|
var downloadURL = opts.downloadURL || "";
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (downloadURL) {
|
|
353
|
-
if (isDirectory || deliverableType === "game") {
|
|
354
|
-
lines.push("文件列表:[查看目录](" + downloadURL + ")");
|
|
355
|
-
} else {
|
|
356
|
-
lines.push("下载链接:[点击下载](" + downloadURL + ")");
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
if (lines.length === 0 && !previewURL && !downloadURL) {
|
|
416
|
+
return previewURL || downloadURL || "";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function buildReplyMarkdown(opts) {
|
|
420
|
+
var primaryURL = resolvePrimaryURL(opts);
|
|
421
|
+
if (!primaryURL) {
|
|
360
422
|
return "交付物已上传成功。";
|
|
361
423
|
}
|
|
362
|
-
return
|
|
424
|
+
return primaryURL;
|
|
363
425
|
}
|
|
364
426
|
|
|
365
427
|
function resolveReplyURLs(data, args, body) {
|
|
@@ -376,12 +438,13 @@ function resolveReplyURLs(data, args, body) {
|
|
|
376
438
|
// ─── Tool implementations ─────────────────────────────────────────────────────
|
|
377
439
|
|
|
378
440
|
function uploadDeliverable(args) {
|
|
441
|
+
var finalFileName = normalizeDeliverableFileName(args.file_name, args.type, args.content_text, args.files);
|
|
379
442
|
var body = {
|
|
380
443
|
resourceId: args.resource_id,
|
|
381
444
|
groupId: args.group_id,
|
|
382
445
|
userId: args.user_id,
|
|
383
446
|
type: args.type,
|
|
384
|
-
fileName:
|
|
447
|
+
fileName: finalFileName,
|
|
385
448
|
release: "" // overwritten below after release name derivation
|
|
386
449
|
};
|
|
387
450
|
|
|
@@ -426,12 +489,21 @@ function uploadDeliverable(args) {
|
|
|
426
489
|
type: args.type,
|
|
427
490
|
isDirectory: isDirectory
|
|
428
491
|
});
|
|
492
|
+
var primaryURL = resolvePrimaryURL({
|
|
493
|
+
previewURL: previewURL,
|
|
494
|
+
downloadURL: downloadURL,
|
|
495
|
+
type: args.type,
|
|
496
|
+
isDirectory: isDirectory
|
|
497
|
+
});
|
|
429
498
|
return {
|
|
430
499
|
uuid: d.uuid,
|
|
500
|
+
file_name: finalFileName,
|
|
431
501
|
backend: d.backend || "",
|
|
432
502
|
download_url: downloadURL,
|
|
433
503
|
preview_url: previewURL,
|
|
504
|
+
primary_url: primaryURL,
|
|
434
505
|
expire_at: d.expireAt,
|
|
506
|
+
is_directory: isDirectory,
|
|
435
507
|
reply_markdown: replyMarkdown,
|
|
436
508
|
trace_id: resp.traceID || "",
|
|
437
509
|
message: replyMarkdown
|
|
@@ -461,7 +533,7 @@ function handleMessage(msg) {
|
|
|
461
533
|
send({ jsonrpc: "2.0", id: id, result: {
|
|
462
534
|
protocolVersion: "2024-11-05",
|
|
463
535
|
capabilities: { tools: {} },
|
|
464
|
-
serverInfo: { name: "deliverables", version: "1.0.
|
|
536
|
+
serverInfo: { name: "deliverables", version: "1.0.14" }
|
|
465
537
|
}});
|
|
466
538
|
return Promise.resolve();
|
|
467
539
|
|
package/openclaw-plugin.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "plugin-deliverables",
|
|
3
|
+
"name": "Deliverables Plugin",
|
|
4
|
+
"version": "1.0.14",
|
|
5
|
+
"description": "Split deliverable replies into summary and link messages for Palz outbound sends.",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {}
|
|
10
|
+
}
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dai_ming/plugin-deliverables",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable
|
|
3
|
+
"version": "1.0.14",
|
|
4
|
+
"description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return a shareable access link",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
7
7
|
"plugin",
|
|
8
8
|
"deliverables",
|
|
9
9
|
"mcp"
|
|
10
10
|
],
|
|
11
|
+
"main": "index.js",
|
|
12
|
+
"openclaw": {
|
|
13
|
+
"extensions": [
|
|
14
|
+
"./index.js"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
11
17
|
"license": "MIT",
|
|
12
18
|
"files": [
|
|
19
|
+
"index.js",
|
|
13
20
|
"install.js",
|
|
21
|
+
"openclaw.plugin.json",
|
|
14
22
|
"openclaw-plugin.json",
|
|
15
23
|
"mcp-servers/",
|
|
16
24
|
"skills/",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: deliverables
|
|
3
|
-
description: 上传AI
|
|
3
|
+
description: 上传AI生成的文件到交付物系统,返回一个可直接分享的访问链接
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 文件交付规则(强制)
|
|
@@ -21,6 +21,8 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
21
21
|
- 多文件内容(游戏/网站)写到 `output/<目录名>/...`,保持目录结构。
|
|
22
22
|
- 如果你使用 `write` 工具创建交付物文件,目标路径必须在 `output/` 下。
|
|
23
23
|
- 如果你已经误写到工作区根目录,必须先移动或复制到 `output/`,再继续上传交付物并向用户汇报。
|
|
24
|
+
- 交付物文件名必须保留原始扩展名,不要丢后缀。示例:`刘德华介绍.md`、`周杰伦.md`、`996专题.html`。
|
|
25
|
+
- 如果用户要求 Markdown,就保留 `.md`;如果用户要求 HTML,就保留 `.html`;如果是纯文本,就保留 `.txt`。
|
|
24
26
|
|
|
25
27
|
## 调用前必须准备的参数(全部必填)
|
|
26
28
|
|
|
@@ -32,7 +34,7 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
32
34
|
| `group_id` | 消息元数据中的 `group_id` 或 `conversation_id` | `group_abc123` |
|
|
33
35
|
| `user_id` | 消息元数据中的 `sender_id` 或 `owner_id` | `cbb0fab9...` |
|
|
34
36
|
| `type` | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`zip`/`link` | `article` |
|
|
35
|
-
| `file_name` |
|
|
37
|
+
| `file_name` | 有意义的文件名,必须含正确扩展名 | `report-2026.html` |
|
|
36
38
|
| `content_text` | 文件的完整文本内容(HTML/Markdown等) | `<html>...</html>` |
|
|
37
39
|
|
|
38
40
|
> **直接对话(direct chat)时**:`group_id` 填 `conversation_id`,`user_id` 填 `owner_id`。
|
|
@@ -65,11 +67,11 @@ files: [
|
|
|
65
67
|
- 这段介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
|
|
66
68
|
- 对于常见文章、介绍、报告类交付物,链接前的介绍通常应达到约 80-200 个中文字符,明显长于一句话确认。
|
|
67
69
|
- bullet 必须写具体内容,例如“基本信息、代表作品、奖项与影响力”,不要只写“内容完整、结构清晰”这类空话。
|
|
68
|
-
- 如果工具结果里有 `reply_markdown`,先输出 `###
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
- 如果工具结果里有 `reply_markdown`,先输出 `### 访问链接`,再把它原样放在后面,不要改写其中的 URL。
|
|
71
|
+
- 否则只输出一个最终链接:
|
|
72
|
+
- 优先 `primary_url`
|
|
73
|
+
- 没有就退回 `preview_url`
|
|
74
|
+
- 再没有才用 `download_url`
|
|
75
|
+
- 不要同时输出“预览链接 + 下载链接”两条。
|
|
76
|
+
|
|
77
|
+
不要在消息里输出文件的完整内容、工作区保存路径,也不要只输出“一句简介 + 两个链接”这种过短格式。
|