@dai_ming/plugin-deliverables 1.1.3 → 1.1.4
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/INSTALL.md +4 -4
- package/README.md +18 -7
- package/agents-rules/deliverables.md +14 -15
- package/deliverables.dev.config.json +4 -0
- package/deliverables.prod.config.json +4 -0
- package/deliverables.staging.config.json +4 -0
- package/index.js +234 -140
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/skills/deliverables/SKILL.md +16 -14
package/INSTALL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# plugin-deliverables 安装文档
|
|
2
2
|
|
|
3
|
-
本文档描述 `@dai_ming/plugin-deliverables@1.1.
|
|
3
|
+
本文档描述 `@dai_ming/plugin-deliverables@1.1.2` 的安装、升级与验证方式。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
openclaw plugins install @dai_ming/plugin-deliverables@1.1.
|
|
8
|
+
openclaw plugins install @dai_ming/plugin-deliverables@1.1.2 --pin
|
|
9
9
|
openclaw plugins enable plugin-deliverables
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ gateway 部署配置:
|
|
|
13
13
|
|
|
14
14
|
```yaml
|
|
15
15
|
installPlugins:
|
|
16
|
-
- "@dai_ming/plugin-deliverables@1.1.
|
|
16
|
+
- "@dai_ming/plugin-deliverables@1.1.2"
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## 验证
|
|
@@ -21,7 +21,7 @@ installPlugins:
|
|
|
21
21
|
1. `openclaw plugins info plugin-deliverables` 能看到插件已启用。
|
|
22
22
|
2. Agent 工具列表里存在 `deliverables__upload_deliverable`。
|
|
23
23
|
3. Pod 内不再出现 `node ...mcp-servers/deliverables.js` 进程。
|
|
24
|
-
4. 生成交付物时,工具返回 `reply_markdown`、`preview_url`、`download_url`。
|
|
24
|
+
4. 生成交付物时,工具返回 `reply_markdown`、`preview_url`、`download_url`、`document_id`。
|
|
25
25
|
|
|
26
26
|
## 说明
|
|
27
27
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dai_ming/plugin-deliverables
|
|
2
2
|
|
|
3
|
-
OpenClaw Native 插件:注册 `deliverables__upload_deliverable` 原生 agent tool,把 AI
|
|
3
|
+
OpenClaw Native 插件:注册 `deliverables__upload_deliverable` 原生 agent tool,把 AI 生成的文件上传到知识库交付物系统,并返回可分享的预览/下载链接。
|
|
4
4
|
|
|
5
5
|
## 包含内容
|
|
6
6
|
|
|
@@ -11,11 +11,12 @@ OpenClaw Native 插件:注册 `deliverables__upload_deliverable` 原生 agent
|
|
|
11
11
|
| `openclaw-plugin.json` | gateway 兼容清单:声明 skill、AGENTS 规则和插件 entry |
|
|
12
12
|
| `skills/deliverables/SKILL.md` | Agent 使用交付物工具的技能说明 |
|
|
13
13
|
| `agents-rules/deliverables.md` | 注入到 workspace `AGENTS.md` 的硬规则 |
|
|
14
|
+
| `deliverables.*.config.json` | 各环境知识库连接配置 |
|
|
14
15
|
|
|
15
16
|
## 安装
|
|
16
17
|
|
|
17
18
|
```bash
|
|
18
|
-
openclaw plugins install @dai_ming/plugin-deliverables@1.1.
|
|
19
|
+
openclaw plugins install @dai_ming/plugin-deliverables@1.1.2 --pin
|
|
19
20
|
openclaw plugins enable plugin-deliverables
|
|
20
21
|
```
|
|
21
22
|
|
|
@@ -23,7 +24,7 @@ openclaw plugins enable plugin-deliverables
|
|
|
23
24
|
|
|
24
25
|
```yaml
|
|
25
26
|
installPlugins:
|
|
26
|
-
- "@dai_ming/plugin-deliverables@1.1.
|
|
27
|
+
- "@dai_ming/plugin-deliverables@1.1.2"
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
## 运行方式
|
|
@@ -34,11 +35,21 @@ installPlugins:
|
|
|
34
35
|
deliverables__upload_deliverable
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
上传工具根据 `HELM_ENV` 环境变量加载对应配置文件:
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
| HELM_ENV | 配置文件 | 说明 |
|
|
41
|
+
|----------|---------|------|
|
|
42
|
+
| (空) | `deliverables.config.json` | 本地开发(127.0.0.1) |
|
|
43
|
+
| `dev` | `deliverables.dev.config.json` | dev 环境 |
|
|
44
|
+
| `staging` | `deliverables.staging.config.json` | staging 环境 |
|
|
45
|
+
| `prod` | `deliverables.prod.config.json` | 生产环境 |
|
|
46
|
+
|
|
47
|
+
配置字段:
|
|
48
|
+
|
|
49
|
+
- `kbBaseUrl` — 知识库 API 内部地址(Pod 内通信)
|
|
50
|
+
- `kbPublicUrl` — 用户可见的预览链接地址
|
|
51
|
+
|
|
52
|
+
环境变量 `KNOWLEDGE_DB_URL` / `KNOWLEDGE_DB_PUBLIC_URL` 可覆盖配置文件值。`KNOWLEDGE_DB_API_KEY` 仅通过环境变量配置(默认 `kb-agent-key-2025`)。
|
|
42
53
|
|
|
43
54
|
## 发布
|
|
44
55
|
|
|
@@ -62,25 +62,24 @@ Tool name note:
|
|
|
62
62
|
7. If the user asks for PDF, upload a `.pdf` deliverable with `type=pdf`; do not substitute HTML or Markdown unless PDF generation truly failed and you clearly say so.
|
|
63
63
|
8. If the upload tool returns an error, retry with valid parameters or report the upload failure. Never invent OSS, preview, or download URLs.
|
|
64
64
|
9. If format is HTML, prefer `type=article` and file name ends with `.html`.
|
|
65
|
-
10.
|
|
66
|
-
11.
|
|
67
|
-
12. If
|
|
68
|
-
13.
|
|
69
|
-
14.
|
|
70
|
-
15.
|
|
71
|
-
16.
|
|
72
|
-
17.
|
|
73
|
-
18.
|
|
74
|
-
19.
|
|
75
|
-
20.
|
|
76
|
-
21. Otherwise final reply MUST include Markdown links (short label + full URL target):
|
|
65
|
+
10. Every single-file deliverable MUST use a file name with an explicit extension.
|
|
66
|
+
11. If format is markdown/text, use `type=article` and `.md`/`.txt`.
|
|
67
|
+
12. If the user did not specify a document/text format, default to Markdown and use a `.md` file name.
|
|
68
|
+
13. 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.
|
|
69
|
+
14. Static multi-file game/site deliverables SHOULD include a root `index.html` so the preview link can open the homepage directly.
|
|
70
|
+
15. 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.
|
|
71
|
+
16. After successful upload, the final assistant message MUST start with 1-2 short sentences in your own words, briefly describing what you generated for the user.
|
|
72
|
+
17. For content deliverables such as articles, documents, reports, and PDFs, add 3-5 concise bullets after the intro summarizing the actual sections, highlights, or conclusions. Do not reply with only a one-sentence upload notice.
|
|
73
|
+
18. The intro and bullets must be based on the actual deliverable content/request, not a fixed sentence like `交付物已上传成功,可直接在线预览或下载。`
|
|
74
|
+
19. If tool result contains `reply_markdown`, append that field verbatim after your intro and content bullets on the following lines.
|
|
75
|
+
20. Otherwise final reply MUST include Markdown links (short label + full URL target):
|
|
77
76
|
`预览链接:[点击预览](<full_url>)`
|
|
78
77
|
`下载链接:[点击下载](<full_url>)`
|
|
79
|
-
|
|
78
|
+
21. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
|
|
80
79
|
`文件列表:[查看目录](<full_url>)`
|
|
81
80
|
Keep that format instead of forcing a zip link.
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
22. Do NOT only say "已保存到工作空间".
|
|
82
|
+
23. Do NOT append workspace path or a raw URL block after the Markdown links.
|
|
84
83
|
|
|
85
84
|
### Exception
|
|
86
85
|
|
package/index.js
CHANGED
|
@@ -11,14 +11,50 @@ const PENDING_FILES_KEY = "__plugin_deliverables_pending_files__";
|
|
|
11
11
|
const UPLOAD_CACHE_KEY = "__plugin_deliverables_upload_cache__";
|
|
12
12
|
const SUMMARY_CACHE_LIMIT = 200;
|
|
13
13
|
const SHORT_SUMMARY_THRESHOLD = 120;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const DEFAULT_KB_BASE_URL = "http://knowledge-db:8080/api/v1";
|
|
15
|
+
const DEFAULT_KB_API_KEY = "kb-agent-key-2025";
|
|
16
|
+
const DEFAULT_GROUP_ID = "default";
|
|
17
|
+
const DEFAULT_GROUP_NAME = "我的文件夹";
|
|
18
|
+
const DEFAULT_DELIVERABLES_CONFIG_FILENAME = "deliverables.config.json";
|
|
19
|
+
let _cachedDeliverableConfig = null;
|
|
20
|
+
|
|
21
|
+
function getDeliverableConfigFilename() {
|
|
22
|
+
const helmEnv = (process.env.HELM_ENV || "").trim();
|
|
23
|
+
return helmEnv
|
|
24
|
+
? "deliverables." + helmEnv + ".config.json"
|
|
25
|
+
: DEFAULT_DELIVERABLES_CONFIG_FILENAME;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadDeliverableConfig() {
|
|
29
|
+
if (_cachedDeliverableConfig) return _cachedDeliverableConfig;
|
|
30
|
+
const helmEnv = (process.env.HELM_ENV || "").trim();
|
|
31
|
+
const configFilename = getDeliverableConfigFilename();
|
|
32
|
+
const candidates = [
|
|
33
|
+
path.resolve(process.cwd(), configFilename),
|
|
34
|
+
path.resolve(__dirname, configFilename),
|
|
35
|
+
];
|
|
36
|
+
for (const filePath of candidates) {
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(filePath)) {
|
|
39
|
+
_cachedDeliverableConfig = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
40
|
+
return _cachedDeliverableConfig;
|
|
41
|
+
}
|
|
42
|
+
} catch (_err) { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
if (helmEnv) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"plugin-deliverables: HELM_ENV=\"" + helmEnv + "\" but config file \"" + configFilename + "\" not found, searched: " + candidates.join(", ")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
_cachedDeliverableConfig = {};
|
|
50
|
+
return _cachedDeliverableConfig;
|
|
51
|
+
}
|
|
16
52
|
const AUTO_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
|
17
53
|
const AUTO_UPLOAD_MAX_FILES = 8;
|
|
18
54
|
const AUTO_UPLOAD_MAX_BYTES = 200 * 1024 * 1024;
|
|
19
55
|
const AUTO_UPLOAD_SCAN_MAX_ENTRIES = 4000;
|
|
20
56
|
const AUTO_UPLOAD_SCAN_DEPTH = 5;
|
|
21
|
-
const
|
|
57
|
+
const DELIVERABLE_KB_PATH_RE = /\/documents\/content\b/u;
|
|
22
58
|
const DELIVERABLE_LINK_PREFIX_RE =
|
|
23
59
|
/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]/u;
|
|
24
60
|
const DELIVERABLE_LINK_LABEL_RE =
|
|
@@ -26,7 +62,6 @@ const DELIVERABLE_LINK_LABEL_RE =
|
|
|
26
62
|
const INTERNAL_ABSOLUTE_PATH_RE =
|
|
27
63
|
/(?:\/data\/workspace-[^\s`"'<>,。;:、))\]}]+|\/home\/node\/\.openclaw\/workspace(?:-[A-Za-z0-9_.-]+)?\/[^\s`"'<>,。;:、))\]}]+)/gu;
|
|
28
64
|
const INLINE_FILE_REF_RE = /`([^`\n]{1,512}\.[A-Za-z0-9]{1,16})`/gu;
|
|
29
|
-
const MARKDOWN_FILE_LINK_RE = /\[[^\]\n]{0,200}\]\(([^)\s]+?\.[A-Za-z0-9]{1,16})\)/gu;
|
|
30
65
|
const OUTPUT_RELATIVE_REF_RE = /(?:^|[\s(::])((?:\.\/)?output\/[^\s`"'<>,。;:、))\]}]+\.[A-Za-z0-9]{1,16})/gu;
|
|
31
66
|
const FILE_EXTENSION_RE = /\.[A-Za-z0-9]{1,16}$/u;
|
|
32
67
|
const TEXT_EXTENSIONS = new Set([
|
|
@@ -81,7 +116,6 @@ const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
|
81
116
|
"- When the user asks for a document, article, report, PDF, HTML page, Markdown file, PPT, image, archive, game, or any other file deliverable, you MUST create it under the current workspace `output/` directory and upload it with `deliverables__upload_deliverable`.",
|
|
82
117
|
"- For simple text PDF deliverables, call `deliverables__upload_deliverable` with `type: \"pdf\"`, a `.pdf` `file_name`, and `content_text`; the tool will render a basic PDF without installing dependencies.",
|
|
83
118
|
"- For existing binary deliverables such as rich PDFs, PPT, images, video, or zip files, pass `file_path` to `deliverables__upload_deliverable` after writing the file under `output/`; do not paste command output or partial base64 into `content_base64`.",
|
|
84
|
-
"- Never use `type: \"link\"` for local files, relative paths, or generated files such as `review.html`; use `file_path`, `content_text`, or `files` so the file is uploaded.",
|
|
85
119
|
"- Never install PDF libraries or other packages at runtime just to create a deliverable. Use built-in tools or the PDF `content_text` fallback; if a rich PDF renderer is unavailable, say so clearly.",
|
|
86
120
|
"- If the user asks for PDF, upload the generated `.pdf` as `type: \"pdf\"`; do not substitute HTML or Markdown unless PDF generation truly failed and you clearly say so.",
|
|
87
121
|
"- Every single-file deliverable must use a file name with an explicit extension. If the user did not specify a document/text format, default to Markdown and use a `.md` file name.",
|
|
@@ -103,20 +137,28 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
103
137
|
properties: {
|
|
104
138
|
resource_id: {
|
|
105
139
|
type: "string",
|
|
106
|
-
description: "当前聊天框/会话的唯一 ID
|
|
140
|
+
description: "当前聊天框/会话的唯一 ID,用于缓存去重。",
|
|
141
|
+
},
|
|
142
|
+
user_id: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "上传者用户 ID。单聊取 owner_id,群聊取 sender_id。",
|
|
145
|
+
},
|
|
146
|
+
owner_id: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "等同于 user_id,从消息元数据 owner_id 字段获取。两者任传其一即可。",
|
|
107
149
|
},
|
|
108
150
|
group_id: {
|
|
109
151
|
type: "string",
|
|
110
|
-
description: "
|
|
152
|
+
description: "分组 ID。单聊不传或传 'default';群聊传群 ID。",
|
|
111
153
|
},
|
|
112
|
-
|
|
154
|
+
group_name: {
|
|
113
155
|
type: "string",
|
|
114
|
-
description: "
|
|
156
|
+
description: "群聊名称,群聊场景必填,用于构造知识库存储路径;单聊不传。",
|
|
115
157
|
},
|
|
116
158
|
type: {
|
|
117
159
|
type: "string",
|
|
118
160
|
enum: ["article", "game", "zip", "image", "video", "ppt", "pdf", "link"],
|
|
119
|
-
description: "
|
|
161
|
+
description: "交付物类型。若使用 file_path 且未准确判断,可由插件按文件扩展名兜底。",
|
|
120
162
|
},
|
|
121
163
|
file_name: {
|
|
122
164
|
type: "string",
|
|
@@ -128,7 +170,7 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
128
170
|
},
|
|
129
171
|
content_text: {
|
|
130
172
|
type: "string",
|
|
131
|
-
description: "文本内容(Markdown、HTML 等)。",
|
|
173
|
+
description: "文本内容(Markdown、HTML 等)。type=pdf 时可传纯文本/Markdown,插件会生成基础 PDF。",
|
|
132
174
|
},
|
|
133
175
|
content_base64: {
|
|
134
176
|
type: "string",
|
|
@@ -148,7 +190,7 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
148
190
|
},
|
|
149
191
|
},
|
|
150
192
|
},
|
|
151
|
-
required: ["
|
|
193
|
+
required: ["type", "file_name"],
|
|
152
194
|
},
|
|
153
195
|
};
|
|
154
196
|
|
|
@@ -199,15 +241,6 @@ function isURLLike(value) {
|
|
|
199
241
|
return /^[a-z][a-z0-9+.-]*:\/\//i.test(String(value || ""));
|
|
200
242
|
}
|
|
201
243
|
|
|
202
|
-
function isHTTPURLLike(value) {
|
|
203
|
-
try {
|
|
204
|
-
const parsed = new URL(String(value || "").trim());
|
|
205
|
-
return (parsed.protocol === "http:" || parsed.protocol === "https:") && !!parsed.hostname;
|
|
206
|
-
} catch (_err) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
244
|
function fileExtension(filePath) {
|
|
212
245
|
return path.extname(stripTrailingPathPunctuation(filePath)).toLowerCase();
|
|
213
246
|
}
|
|
@@ -241,10 +274,14 @@ function deliverableTypeForPath(filePath, isDirectory) {
|
|
|
241
274
|
|
|
242
275
|
function isAllowedWorkspacePath(candidatePath) {
|
|
243
276
|
const normalized = normalizeSlash(path.resolve(candidatePath));
|
|
244
|
-
|
|
277
|
+
if (
|
|
245
278
|
normalized.indexOf("/data/workspace-") === 0 ||
|
|
246
279
|
normalized.indexOf("/home/node/.openclaw/workspace") === 0
|
|
247
|
-
)
|
|
280
|
+
) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
const openclawRoot = normalizeSlash(path.resolve(__dirname, "..", ".."));
|
|
284
|
+
return normalized.indexOf(openclawRoot + "/workspace") === 0;
|
|
248
285
|
}
|
|
249
286
|
|
|
250
287
|
function shouldIgnorePath(candidatePath) {
|
|
@@ -286,6 +323,20 @@ function workspaceRoots() {
|
|
|
286
323
|
} catch (_err) {
|
|
287
324
|
// /data is not guaranteed in non-pod test environments.
|
|
288
325
|
}
|
|
326
|
+
const openclawRoot = path.resolve(__dirname, "..", "..");
|
|
327
|
+
try {
|
|
328
|
+
for (const entry of fs.readdirSync(openclawRoot)) {
|
|
329
|
+
if (!entry || entry.indexOf("workspace") !== 0) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const root = path.join(openclawRoot, entry);
|
|
333
|
+
if (safeStat(root) && roots.indexOf(root) === -1) {
|
|
334
|
+
roots.push(root);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch (_err) {
|
|
338
|
+
// local dev path may not exist
|
|
339
|
+
}
|
|
289
340
|
return roots;
|
|
290
341
|
}
|
|
291
342
|
|
|
@@ -384,9 +435,6 @@ function extractFileReferencesFromText(text) {
|
|
|
384
435
|
for (const match of normalized.matchAll(INLINE_FILE_REF_RE)) {
|
|
385
436
|
add(match[0], match[1], "inline");
|
|
386
437
|
}
|
|
387
|
-
for (const match of normalized.matchAll(MARKDOWN_FILE_LINK_RE)) {
|
|
388
|
-
add(match[0], match[1], "markdown-link");
|
|
389
|
-
}
|
|
390
438
|
for (const match of normalized.matchAll(OUTPUT_RELATIVE_REF_RE)) {
|
|
391
439
|
add(match[1], match[1], "relative");
|
|
392
440
|
}
|
|
@@ -505,7 +553,7 @@ function payloadResourceId(body) {
|
|
|
505
553
|
}
|
|
506
554
|
|
|
507
555
|
function payloadGroupId(body) {
|
|
508
|
-
return extractStringField(body, ["group_id", "groupId"
|
|
556
|
+
return extractStringField(body, ["group_id", "groupId"]);
|
|
509
557
|
}
|
|
510
558
|
|
|
511
559
|
function payloadUserId(body) {
|
|
@@ -572,76 +620,128 @@ function pendingCandidatesForPayload(body) {
|
|
|
572
620
|
return out;
|
|
573
621
|
}
|
|
574
622
|
|
|
575
|
-
function
|
|
576
|
-
|
|
623
|
+
function kbBaseURL() {
|
|
624
|
+
const cfg = loadDeliverableConfig();
|
|
625
|
+
return (process.env.KNOWLEDGE_DB_URL || cfg.kbBaseUrl || DEFAULT_KB_BASE_URL).replace(/\/$/, "");
|
|
577
626
|
}
|
|
578
627
|
|
|
579
|
-
function
|
|
580
|
-
|
|
628
|
+
function kbPublicURL() {
|
|
629
|
+
const cfg = loadDeliverableConfig();
|
|
630
|
+
return (process.env.KNOWLEDGE_DB_PUBLIC_URL || cfg.kbPublicUrl || process.env.KNOWLEDGE_DB_URL || cfg.kbBaseUrl || DEFAULT_KB_BASE_URL).replace(/\/$/, "");
|
|
581
631
|
}
|
|
582
632
|
|
|
583
|
-
function
|
|
584
|
-
return process.env.
|
|
633
|
+
function kbApiKey() {
|
|
634
|
+
return process.env.KNOWLEDGE_DB_API_KEY || DEFAULT_KB_API_KEY;
|
|
585
635
|
}
|
|
586
636
|
|
|
587
|
-
function
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
return release;
|
|
637
|
+
function kbUploadPath(groupId, groupName) {
|
|
638
|
+
const gid = trimString(groupId) || DEFAULT_GROUP_ID;
|
|
639
|
+
const gname = gid === DEFAULT_GROUP_ID
|
|
640
|
+
? DEFAULT_GROUP_NAME
|
|
641
|
+
: trimString(groupName) || gid;
|
|
642
|
+
return "/" + gname + "/raw";
|
|
596
643
|
}
|
|
597
644
|
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (raw.indexOf("user_") === 0 && raw.indexOf("_lobster_") > 0) {
|
|
604
|
-
raw = raw.slice("user_".length, raw.indexOf("_lobster_"));
|
|
605
|
-
} else if (raw.indexOf("user-") === 0 || raw.indexOf("user_") === 0) {
|
|
606
|
-
raw = raw.slice("user-".length);
|
|
607
|
-
}
|
|
608
|
-
return raw;
|
|
645
|
+
function kbPreviewURL(documentId, userId, groupId) {
|
|
646
|
+
return kbPublicURL() + "/documents/content?user_id="
|
|
647
|
+
+ encodeURIComponent(trimString(userId) || "default")
|
|
648
|
+
+ "&group_id=" + encodeURIComponent(trimString(groupId) || DEFAULT_GROUP_ID)
|
|
649
|
+
+ "&id=" + encodeURIComponent(String(documentId));
|
|
609
650
|
}
|
|
610
651
|
|
|
611
|
-
function
|
|
612
|
-
|
|
652
|
+
function kbExtractDocumentId(response) {
|
|
653
|
+
const data = (response && response.data) || response || {};
|
|
654
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
655
|
+
if (items.length > 0 && items[0].document && items[0].document.id !== undefined) {
|
|
656
|
+
return items[0].document.id;
|
|
657
|
+
}
|
|
658
|
+
if (data.id !== undefined) {
|
|
659
|
+
return data.id;
|
|
660
|
+
}
|
|
661
|
+
return null;
|
|
613
662
|
}
|
|
614
663
|
|
|
615
|
-
function
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
664
|
+
function buildFileBuffersFromBody(body, isDirectory) {
|
|
665
|
+
if (isDirectory && Array.isArray(body.files)) {
|
|
666
|
+
return body.files.map((file) => ({
|
|
667
|
+
fileName: file.name || "file",
|
|
668
|
+
content: file.contentText
|
|
669
|
+
? Buffer.from(file.contentText, "utf8")
|
|
670
|
+
: Buffer.from(file.contentBase64 || "", "base64"),
|
|
671
|
+
}));
|
|
619
672
|
}
|
|
620
|
-
|
|
673
|
+
const content = body.contentBase64
|
|
674
|
+
? Buffer.from(body.contentBase64, "base64")
|
|
675
|
+
: Buffer.from(body.contentText || "", "utf8");
|
|
676
|
+
return [{ fileName: body.fileName || "file", content }];
|
|
621
677
|
}
|
|
622
678
|
|
|
623
|
-
function
|
|
679
|
+
function kbMultipartUpload(fileBuffers, fields, pathsArray) {
|
|
624
680
|
return new Promise((resolve, reject) => {
|
|
625
681
|
let parsed;
|
|
626
682
|
try {
|
|
627
|
-
parsed = new URL(
|
|
683
|
+
parsed = new URL(kbBaseURL() + "/documents/batch-upload");
|
|
628
684
|
} catch (err) {
|
|
629
685
|
reject(err);
|
|
630
686
|
return;
|
|
631
687
|
}
|
|
688
|
+
const boundary = "----Deliverables" + Date.now() + Math.random().toString(36).slice(2);
|
|
689
|
+
const parts = [];
|
|
690
|
+
|
|
691
|
+
for (const file of fileBuffers) {
|
|
692
|
+
parts.push(
|
|
693
|
+
Buffer.from(
|
|
694
|
+
"--" + boundary + "\r\n" +
|
|
695
|
+
'Content-Disposition: form-data; name="files"; filename="' + file.fileName + '"\r\n' +
|
|
696
|
+
"Content-Type: application/octet-stream\r\n\r\n",
|
|
697
|
+
),
|
|
698
|
+
);
|
|
699
|
+
parts.push(file.content);
|
|
700
|
+
parts.push(Buffer.from("\r\n"));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (Array.isArray(pathsArray) && pathsArray.length > 0) {
|
|
704
|
+
for (const p of pathsArray) {
|
|
705
|
+
parts.push(
|
|
706
|
+
Buffer.from(
|
|
707
|
+
"--" + boundary + "\r\n" +
|
|
708
|
+
'Content-Disposition: form-data; name="paths"\r\n\r\n' +
|
|
709
|
+
(p || "") + "\r\n",
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
716
|
+
if (value === undefined || value === null || value === "") {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
parts.push(
|
|
720
|
+
Buffer.from(
|
|
721
|
+
"--" + boundary + "\r\n" +
|
|
722
|
+
'Content-Disposition: form-data; name="' + key + '"\r\n\r\n' +
|
|
723
|
+
String(value) + "\r\n",
|
|
724
|
+
),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
parts.push(Buffer.from("--" + boundary + "--\r\n"));
|
|
729
|
+
const body = Buffer.concat(parts);
|
|
730
|
+
|
|
632
731
|
const isHTTPS = parsed.protocol === "https:";
|
|
633
732
|
const transport = isHTTPS ? https : http;
|
|
634
|
-
const
|
|
733
|
+
const userId = (fields && fields.user_id) || "default";
|
|
635
734
|
const req = transport.request(
|
|
636
735
|
{
|
|
637
736
|
hostname: parsed.hostname,
|
|
638
737
|
port: parsed.port || (isHTTPS ? 443 : 80),
|
|
639
738
|
path: parsed.pathname + (parsed.search || ""),
|
|
640
|
-
method,
|
|
739
|
+
method: "POST",
|
|
641
740
|
headers: {
|
|
642
|
-
"Content-Type": "
|
|
643
|
-
"X-
|
|
644
|
-
"
|
|
741
|
+
"Content-Type": "multipart/form-data; boundary=" + boundary,
|
|
742
|
+
"X-User-ID": userId,
|
|
743
|
+
"X-API-Key": kbApiKey(),
|
|
744
|
+
"Content-Length": body.length,
|
|
645
745
|
},
|
|
646
746
|
},
|
|
647
747
|
(res) => {
|
|
@@ -653,11 +753,15 @@ function httpJSONRequest(method, requestPath, body) {
|
|
|
653
753
|
try {
|
|
654
754
|
obj = JSON.parse(text);
|
|
655
755
|
} catch (_err) {
|
|
656
|
-
reject(new Error(`
|
|
756
|
+
reject(new Error(`kb ${res.statusCode}: non-JSON response: ${text.slice(0, 200)}`));
|
|
657
757
|
return;
|
|
658
758
|
}
|
|
659
759
|
if (res.statusCode >= 400) {
|
|
660
|
-
reject(new Error(`
|
|
760
|
+
reject(new Error(`kb ${res.statusCode}: ${obj.message || text.slice(0, 200)}`));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (obj.code !== 0 && obj.code !== undefined && res.statusCode !== 206) {
|
|
764
|
+
reject(new Error(`kb API error: ${obj.message || text.slice(0, 200)}`));
|
|
661
765
|
return;
|
|
662
766
|
}
|
|
663
767
|
resolve(obj);
|
|
@@ -665,9 +769,7 @@ function httpJSONRequest(method, requestPath, body) {
|
|
|
665
769
|
},
|
|
666
770
|
);
|
|
667
771
|
req.on("error", reject);
|
|
668
|
-
|
|
669
|
-
req.write(bodyStr);
|
|
670
|
-
}
|
|
772
|
+
req.write(body);
|
|
671
773
|
req.end();
|
|
672
774
|
});
|
|
673
775
|
}
|
|
@@ -888,7 +990,7 @@ function buildReplyMarkdown(opts) {
|
|
|
888
990
|
if (previewURL) {
|
|
889
991
|
lines.push(`预览链接:[点击预览](${previewURL})`);
|
|
890
992
|
}
|
|
891
|
-
if (downloadURL) {
|
|
993
|
+
if (downloadURL && downloadURL !== previewURL) {
|
|
892
994
|
if (isDirectory || deliverableType === "game") {
|
|
893
995
|
lines.push(`文件列表:[查看目录](${downloadURL})`);
|
|
894
996
|
} else {
|
|
@@ -922,24 +1024,12 @@ function buildNativeUploadRequestBody(args) {
|
|
|
922
1024
|
}
|
|
923
1025
|
}
|
|
924
1026
|
|
|
925
|
-
const body = {
|
|
926
|
-
resourceId: trimString(args && args.resource_id),
|
|
927
|
-
groupId: trimString(args && args.group_id) || trimString(args && args.resource_id),
|
|
928
|
-
userId:
|
|
929
|
-
normalizeBareUserID(args && args.user_id) ||
|
|
930
|
-
userIDFromResourceID(args && args.resource_id) ||
|
|
931
|
-
userIDFromRelease(deriveReleaseName()),
|
|
932
|
-
release: deriveReleaseName(),
|
|
933
|
-
};
|
|
1027
|
+
const body = {};
|
|
934
1028
|
|
|
935
1029
|
if (fileCandidate) {
|
|
936
1030
|
const stat = safeStat(fileCandidate.path);
|
|
937
1031
|
const isDirectory = !!(stat && stat.isDirectory());
|
|
938
|
-
|
|
939
|
-
body.type =
|
|
940
|
-
requestedType === "link"
|
|
941
|
-
? deliverableTypeForPath(fileCandidate.path, isDirectory)
|
|
942
|
-
: requestedType || deliverableTypeForPath(fileCandidate.path, isDirectory);
|
|
1032
|
+
body.type = trimString(args.type) || deliverableTypeForPath(fileCandidate.path, isDirectory);
|
|
943
1033
|
body.fileName = normalizeDeliverableFileName(args, fileCandidate.fileName);
|
|
944
1034
|
if (isDirectory) {
|
|
945
1035
|
const files = collectDirectoryFiles(fileCandidate.path);
|
|
@@ -959,23 +1049,11 @@ function buildNativeUploadRequestBody(args) {
|
|
|
959
1049
|
body.fileName = normalizeDeliverableFileName(args, "");
|
|
960
1050
|
const files = normalizeNativeToolFiles(args && args.files);
|
|
961
1051
|
if (files.length > 0) {
|
|
962
|
-
if (body.type === "link") {
|
|
963
|
-
body.type = "game";
|
|
964
|
-
}
|
|
965
1052
|
body.files = files;
|
|
966
1053
|
return { body, isDirectory: true };
|
|
967
1054
|
}
|
|
968
1055
|
const contentText = (args && (args.content_text || args.contentText)) || "";
|
|
969
1056
|
const contentBase64 = (args && (args.content_base64 || args.contentBase64)) || "";
|
|
970
|
-
if (body.type === "link") {
|
|
971
|
-
if (trimString(contentText) || trimString(contentBase64)) {
|
|
972
|
-
body.type = deliverableTypeForPath(body.fileName, false);
|
|
973
|
-
} else if (!isHTTPURLLike(body.fileName)) {
|
|
974
|
-
throw new Error(
|
|
975
|
-
"type=link requires file_name to be an absolute http(s) URL; use file_path or content_text for local/generated files",
|
|
976
|
-
);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
1057
|
if (body.type === "pdf" && trimString(contentText) && !trimString(contentBase64)) {
|
|
980
1058
|
body.contentBase64 = createBasicTextPDF(contentText, body.fileName).toString("base64");
|
|
981
1059
|
} else {
|
|
@@ -987,10 +1065,20 @@ function buildNativeUploadRequestBody(args) {
|
|
|
987
1065
|
|
|
988
1066
|
async function uploadDeliverable(args) {
|
|
989
1067
|
const { body, isDirectory } = buildNativeUploadRequestBody(args || {});
|
|
990
|
-
const
|
|
991
|
-
const
|
|
992
|
-
const
|
|
993
|
-
const
|
|
1068
|
+
const userId = payloadUserId(args) || "default";
|
|
1069
|
+
const groupId = trimString(args && args.group_id) || DEFAULT_GROUP_ID;
|
|
1070
|
+
const groupName = trimString(args && args.group_name);
|
|
1071
|
+
const uploadPath = kbUploadPath(groupId, groupName);
|
|
1072
|
+
const fileBuffers = buildFileBuffersFromBody(body, isDirectory);
|
|
1073
|
+
const fields = {
|
|
1074
|
+
user_id: userId,
|
|
1075
|
+
group_id: groupId,
|
|
1076
|
+
path: uploadPath,
|
|
1077
|
+
};
|
|
1078
|
+
const response = await kbMultipartUpload(fileBuffers, fields);
|
|
1079
|
+
const documentId = kbExtractDocumentId(response);
|
|
1080
|
+
const previewURL = documentId ? kbPreviewURL(documentId, userId, groupId) : "";
|
|
1081
|
+
const downloadURL = previewURL;
|
|
994
1082
|
const replyMarkdown = buildReplyMarkdown({
|
|
995
1083
|
previewURL,
|
|
996
1084
|
downloadURL,
|
|
@@ -998,11 +1086,9 @@ async function uploadDeliverable(args) {
|
|
|
998
1086
|
isDirectory,
|
|
999
1087
|
});
|
|
1000
1088
|
return {
|
|
1001
|
-
|
|
1002
|
-
backend: data.backend || "",
|
|
1089
|
+
document_id: documentId,
|
|
1003
1090
|
download_url: downloadURL,
|
|
1004
1091
|
preview_url: previewURL,
|
|
1005
|
-
expire_at: data.expireAt,
|
|
1006
1092
|
reply_markdown: replyMarkdown,
|
|
1007
1093
|
message: replyMarkdown,
|
|
1008
1094
|
};
|
|
@@ -1032,35 +1118,41 @@ async function uploadCandidate(candidate, body) {
|
|
|
1032
1118
|
if (!stat) {
|
|
1033
1119
|
throw new Error(`file no longer exists: ${candidate.fileName}`);
|
|
1034
1120
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
};
|
|
1121
|
+
|
|
1122
|
+
const userId = payloadUserId(body) || "default";
|
|
1123
|
+
const groupId = payloadGroupId(body) || DEFAULT_GROUP_ID;
|
|
1124
|
+
const fileName = candidate.fileName || path.basename(candidate.path);
|
|
1125
|
+
const uploadPath = kbUploadPath(groupId, "");
|
|
1126
|
+
|
|
1127
|
+
let fileBuffers;
|
|
1043
1128
|
if (stat.isDirectory()) {
|
|
1044
1129
|
const files = collectDirectoryFiles(candidate.path);
|
|
1045
1130
|
if (files.length === 0) {
|
|
1046
1131
|
throw new Error(`directory has no uploadable files: ${candidate.fileName}`);
|
|
1047
1132
|
}
|
|
1048
|
-
|
|
1133
|
+
fileBuffers = files.map((f) => ({
|
|
1134
|
+
fileName: f.name,
|
|
1135
|
+
content: f.contentText ? Buffer.from(f.contentText, "utf8") : Buffer.from(f.contentBase64 || "", "base64"),
|
|
1136
|
+
}));
|
|
1049
1137
|
} else {
|
|
1050
1138
|
if (stat.size > AUTO_UPLOAD_MAX_BYTES) {
|
|
1051
1139
|
throw new Error(`file exceeds auto-upload limit: ${candidate.fileName}`);
|
|
1052
1140
|
}
|
|
1053
|
-
|
|
1141
|
+
fileBuffers = [{ fileName, content: fs.readFileSync(candidate.path) }];
|
|
1054
1142
|
}
|
|
1055
1143
|
|
|
1056
|
-
const response = await
|
|
1057
|
-
|
|
1058
|
-
|
|
1144
|
+
const response = await kbMultipartUpload(fileBuffers, {
|
|
1145
|
+
user_id: userId,
|
|
1146
|
+
group_id: groupId,
|
|
1147
|
+
path: uploadPath,
|
|
1148
|
+
});
|
|
1149
|
+
const documentId = kbExtractDocumentId(response);
|
|
1150
|
+
const previewURL = documentId ? kbPreviewURL(documentId, userId, groupId) : "";
|
|
1059
1151
|
const result = {
|
|
1060
|
-
fileName
|
|
1061
|
-
|
|
1152
|
+
fileName,
|
|
1153
|
+
document_id: documentId,
|
|
1062
1154
|
previewURL,
|
|
1063
|
-
downloadURL:
|
|
1155
|
+
downloadURL: previewURL,
|
|
1064
1156
|
};
|
|
1065
1157
|
cache.set(cacheKey, { result, createdAt: Date.now() });
|
|
1066
1158
|
return result;
|
|
@@ -1552,12 +1644,14 @@ function extractDeliverableURL(line) {
|
|
|
1552
1644
|
return "";
|
|
1553
1645
|
}
|
|
1554
1646
|
|
|
1555
|
-
function
|
|
1647
|
+
function configuredKBHosts() {
|
|
1648
|
+
const cfg = loadDeliverableConfig();
|
|
1556
1649
|
const values = [
|
|
1557
|
-
process.env.
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1650
|
+
process.env.KNOWLEDGE_DB_PUBLIC_URL,
|
|
1651
|
+
cfg.kbPublicUrl,
|
|
1652
|
+
process.env.KNOWLEDGE_DB_URL,
|
|
1653
|
+
cfg.kbBaseUrl,
|
|
1654
|
+
DEFAULT_KB_BASE_URL,
|
|
1561
1655
|
];
|
|
1562
1656
|
const hosts = new Set();
|
|
1563
1657
|
for (const value of values) {
|
|
@@ -1585,10 +1679,10 @@ function isTrustedDeliverableURL(rawURL) {
|
|
|
1585
1679
|
} catch (_err) {
|
|
1586
1680
|
return false;
|
|
1587
1681
|
}
|
|
1588
|
-
if (!
|
|
1682
|
+
if (!DELIVERABLE_KB_PATH_RE.test(parsed.pathname)) {
|
|
1589
1683
|
return false;
|
|
1590
1684
|
}
|
|
1591
|
-
const hosts =
|
|
1685
|
+
const hosts = configuredKBHosts();
|
|
1592
1686
|
const host = parsed.host.toLowerCase();
|
|
1593
1687
|
const hostname = parsed.hostname.toLowerCase();
|
|
1594
1688
|
return hosts.has(host) || hosts.has(hostname);
|
|
@@ -1716,17 +1810,11 @@ function extractDeliverableIdentity(rawURL) {
|
|
|
1716
1810
|
} catch (_err) {
|
|
1717
1811
|
return "";
|
|
1718
1812
|
}
|
|
1719
|
-
if (!
|
|
1813
|
+
if (!DELIVERABLE_KB_PATH_RE.test(parsed.pathname)) {
|
|
1720
1814
|
return "";
|
|
1721
1815
|
}
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
for (const part of parts) {
|
|
1725
|
-
if (uuidRe.test(part)) {
|
|
1726
|
-
return part.toLowerCase();
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
return parsed.href;
|
|
1816
|
+
const docId = parsed.searchParams.get("id");
|
|
1817
|
+
return docId ? String(docId) : "";
|
|
1730
1818
|
}
|
|
1731
1819
|
|
|
1732
1820
|
function selectPalzFileURL(linkItems) {
|
|
@@ -1768,7 +1856,7 @@ function cloneBodyAsFileLink(body, fileUrl, suffix) {
|
|
|
1768
1856
|
function cloneBodyAsBlockedDeliverableLink(body) {
|
|
1769
1857
|
return cloneBody(
|
|
1770
1858
|
body,
|
|
1771
|
-
"
|
|
1859
|
+
"交付物上传未成功,已阻止发送未通过知识库交付物系统生成的文件链接。请重新通过交付物上传工具上传后再发送。",
|
|
1772
1860
|
"",
|
|
1773
1861
|
);
|
|
1774
1862
|
}
|
|
@@ -2009,15 +2097,21 @@ const plugin = {
|
|
|
2009
2097
|
};
|
|
2010
2098
|
|
|
2011
2099
|
plugin.__test = {
|
|
2100
|
+
buildFileBuffersFromBody,
|
|
2012
2101
|
buildNativeUploadRequestBody,
|
|
2013
2102
|
createBasicTextPDF,
|
|
2014
2103
|
deliverableTypeForPath,
|
|
2015
|
-
extractFileReferencesFromText,
|
|
2016
2104
|
extractDeliverableURL,
|
|
2105
|
+
extractFileReferencesFromText,
|
|
2017
2106
|
findUntrustedOSSDeliverableURL,
|
|
2018
2107
|
isDeliverableLinkLine,
|
|
2019
2108
|
isOSSLikeURL,
|
|
2020
2109
|
isTrustedDeliverableURL,
|
|
2110
|
+
kbExtractDocumentId,
|
|
2111
|
+
kbMultipartUpload,
|
|
2112
|
+
kbPreviewURL,
|
|
2113
|
+
kbUploadPath,
|
|
2114
|
+
loadDeliverableConfig,
|
|
2021
2115
|
selectPalzFileURL,
|
|
2022
2116
|
splitDeliverableMessage,
|
|
2023
2117
|
};
|
package/openclaw-plugin.json
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "plugin-deliverables",
|
|
3
3
|
"name": "Deliverables",
|
|
4
4
|
"description": "Deliverables runtime guard for upload-first file delivery with Palz split-send diagnostics.",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.4",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dai_ming/plugin-deliverables",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "OpenClaw deliverables native plugin — upload AI-generated files to OSS and return shareable preview/download links",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"INSTALL.md",
|
|
17
17
|
"index.js",
|
|
18
|
+
"deliverables.*.config.json",
|
|
18
19
|
"openclaw.plugin.json",
|
|
19
20
|
"openclaw-plugin.json",
|
|
20
21
|
"skills/",
|
|
@@ -22,29 +22,31 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
22
22
|
- 如果你使用 `write` 工具创建交付物文件,目标路径必须在 `output/` 下。
|
|
23
23
|
- 如果你已经误写到工作区根目录,必须先移动或复制到 `output/`,再继续上传交付物并向用户汇报。
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## 调用前必须准备的参数
|
|
26
26
|
|
|
27
27
|
从当前会话上下文提取以下值,**调用时一次性填完,不要留空**:
|
|
28
28
|
|
|
29
|
-
| 参数 | 取值来源 | 示例 |
|
|
30
|
-
|
|
31
|
-
| `
|
|
32
|
-
| `group_id` |
|
|
33
|
-
| `
|
|
34
|
-
| `type` | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`pdf`/`zip
|
|
35
|
-
| `file_name` | 有意义的文件名,单文件必须含扩展名;若用户未指定文档格式,默认用 `.md` | `report-2026.html` |
|
|
36
|
-
| `content_text` | 文件的完整文本内容(HTML/Markdown
|
|
37
|
-
| `file_path` | PDF/PPT/图片/zip 等二进制文件的本地路径,推荐使用,避免手工复制 base64 | `output/sample.pdf` |
|
|
29
|
+
| 参数 | 必填 | 取值来源 | 示例 |
|
|
30
|
+
|------|------|---------|------|
|
|
31
|
+
| `user_id` | 是 | 单聊取 `owner_id`,群聊取 `sender_id` | `cbb0fab9...` |
|
|
32
|
+
| `group_id` | 群聊时必填 | 群聊取消息元数据中的 `group_id`;**单聊不传**(自动为 `default`) | `grp_43c75713` |
|
|
33
|
+
| `group_name` | 群聊时必填 | 群聊取消息元数据中的群聊名称;**单聊不传** | `项目讨论群` |
|
|
34
|
+
| `type` | 是 | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`pdf`/`zip`/`link` | `article` |
|
|
35
|
+
| `file_name` | 是 | 有意义的文件名,单文件必须含扩展名;若用户未指定文档格式,默认用 `.md` | `report-2026.html` |
|
|
36
|
+
| `content_text` | 按需 | 文件的完整文本内容(HTML/Markdown等);简单 PDF 可传文本/Markdown,由工具生成基础 PDF | `<html>...</html>` |
|
|
37
|
+
| `file_path` | 按需 | PDF/PPT/图片/zip 等二进制文件的本地路径,推荐使用,避免手工复制 base64 | `output/sample.pdf` |
|
|
38
38
|
|
|
39
|
-
>
|
|
39
|
+
> **单聊(direct chat)**:`user_id` 填 `owner_id`,不传 `group_id` 和 `group_name`。
|
|
40
|
+
> **群聊(group chat)**:`user_id` 填 `sender_id`,`group_id` 和 `group_name` 从群聊元数据获取。
|
|
40
41
|
|
|
41
42
|
## 二进制文件上传(强制)
|
|
42
43
|
|
|
43
|
-
- PDF
|
|
44
|
+
- 简单文本 PDF 可以直接调用 `deliverables__upload_deliverable`:`type=pdf`、`.pdf` 文件名、`content_text` 填完整内容;工具会生成基础 PDF,不需要额外依赖。
|
|
45
|
+
- 已经生成好的 PDF、PPT、图片、视频、zip 等二进制文件必须优先传 `file_path`,不要把 `base64` 命令输出复制到 `content_base64`。
|
|
44
46
|
- `file_path` 指向你已经写入 `output/` 的文件,例如 `output/sample.pdf`;上传工具会读取文件并自动编码。
|
|
45
|
-
-
|
|
47
|
+
- 不要为了生成 PDF 临时执行 `pip install`、`npm install`、`apt-get` 等依赖安装。使用已有工具或上面的 PDF `content_text` 兜底;如果复杂 PDF 渲染器不可用,要明确说明限制。
|
|
48
|
+
- 用户要求 PDF 时,必须上传 `.pdf` 交付物,`type` 使用 `pdf`;不要只返回 HTML/Markdown 作为替代格式。
|
|
46
49
|
- 如果上传工具返回错误,必须修正参数后重试,或明确告诉用户上传失败;禁止编造 OSS、下载或预览链接。
|
|
47
|
-
- 本地文件名或相对路径(例如 `review.html`、`output/report.md`)不是外部链接,不能用 `type=link`;必须用 `file_path` 或 `content_text` 上传。
|
|
48
50
|
|
|
49
51
|
## 多文件(游戏)
|
|
50
52
|
|