@insta-dev01/insta-plugin-openclaw 1.0.0 → 1.0.2

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.
Files changed (127) hide show
  1. package/dist/index.d.ts +9 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +171 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/channel/config.d.ts +9 -0
  6. package/dist/src/channel/config.d.ts.map +1 -0
  7. package/dist/src/channel/config.js +10 -0
  8. package/dist/src/channel/config.js.map +1 -0
  9. package/dist/src/channel/connection.d.ts +34 -0
  10. package/dist/src/channel/connection.d.ts.map +1 -0
  11. package/dist/src/channel/connection.js +281 -0
  12. package/dist/src/channel/connection.js.map +1 -0
  13. package/dist/src/channel/dispatcher.d.ts +43 -0
  14. package/dist/src/channel/dispatcher.d.ts.map +1 -0
  15. package/dist/src/channel/dispatcher.js +324 -0
  16. package/dist/src/channel/dispatcher.js.map +1 -0
  17. package/dist/src/channel/index.d.ts +5 -0
  18. package/dist/src/channel/index.d.ts.map +1 -0
  19. package/dist/src/channel/index.js +135 -0
  20. package/dist/src/channel/index.js.map +1 -0
  21. package/dist/src/channel/logger.d.ts +10 -0
  22. package/dist/src/channel/logger.d.ts.map +1 -0
  23. package/dist/src/channel/logger.js +30 -0
  24. package/dist/src/channel/logger.js.map +1 -0
  25. package/dist/src/channel/protocol.d.ts +15 -0
  26. package/dist/src/channel/protocol.d.ts.map +1 -0
  27. package/dist/src/channel/protocol.js +204 -0
  28. package/dist/src/channel/protocol.js.map +1 -0
  29. package/dist/src/channel/registrar.d.ts +21 -0
  30. package/dist/src/channel/registrar.d.ts.map +1 -0
  31. package/dist/src/channel/registrar.js +115 -0
  32. package/dist/src/channel/registrar.js.map +1 -0
  33. package/dist/src/channel/registration-store.d.ts +21 -0
  34. package/dist/src/channel/registration-store.d.ts.map +1 -0
  35. package/{src/channel/registration-store.ts → dist/src/channel/registration-store.js} +21 -46
  36. package/dist/src/channel/registration-store.js.map +1 -0
  37. package/dist/src/channel/types.d.ts +80 -0
  38. package/dist/src/channel/types.d.ts.map +1 -0
  39. package/dist/src/channel/types.js +3 -0
  40. package/dist/src/channel/types.js.map +1 -0
  41. package/dist/src/core/index.d.ts +5 -0
  42. package/dist/src/core/index.d.ts.map +1 -0
  43. package/dist/src/core/index.js +3 -0
  44. package/dist/src/core/index.js.map +1 -0
  45. package/dist/src/core/register-identity.d.ts +58 -0
  46. package/dist/src/core/register-identity.d.ts.map +1 -0
  47. package/dist/src/core/register-identity.js +251 -0
  48. package/dist/src/core/register-identity.js.map +1 -0
  49. package/dist/src/core/task-api.d.ts +31 -0
  50. package/dist/src/core/task-api.d.ts.map +1 -0
  51. package/dist/src/core/task-api.js +116 -0
  52. package/dist/src/core/task-api.js.map +1 -0
  53. package/dist/src/core/urls.d.ts +33 -0
  54. package/dist/src/core/urls.d.ts.map +1 -0
  55. package/dist/src/core/urls.js +40 -0
  56. package/dist/src/core/urls.js.map +1 -0
  57. package/dist/src/tools/get-plugin-profile.d.ts +7 -0
  58. package/dist/src/tools/get-plugin-profile.d.ts.map +1 -0
  59. package/dist/src/tools/get-plugin-profile.js +132 -0
  60. package/dist/src/tools/get-plugin-profile.js.map +1 -0
  61. package/dist/src/tools/grab-task.d.ts +7 -0
  62. package/dist/src/tools/grab-task.d.ts.map +1 -0
  63. package/dist/src/tools/grab-task.js +100 -0
  64. package/dist/src/tools/grab-task.js.map +1 -0
  65. package/dist/src/tools/list-tasks.d.ts +7 -0
  66. package/dist/src/tools/list-tasks.d.ts.map +1 -0
  67. package/dist/src/tools/list-tasks.js +92 -0
  68. package/dist/src/tools/list-tasks.js.map +1 -0
  69. package/dist/src/tools/propose-registration.d.ts +14 -0
  70. package/dist/src/tools/propose-registration.d.ts.map +1 -0
  71. package/dist/src/tools/propose-registration.js +103 -0
  72. package/dist/src/tools/propose-registration.js.map +1 -0
  73. package/dist/src/tools/register-identity.d.ts +11 -0
  74. package/dist/src/tools/register-identity.d.ts.map +1 -0
  75. package/dist/src/tools/register-identity.js +101 -0
  76. package/dist/src/tools/register-identity.js.map +1 -0
  77. package/dist/src/tools/submit-deliverable.d.ts +17 -0
  78. package/dist/src/tools/submit-deliverable.d.ts.map +1 -0
  79. package/dist/src/tools/submit-deliverable.js +215 -0
  80. package/dist/src/tools/submit-deliverable.js.map +1 -0
  81. package/dist/src/tools/upload-artifact.d.ts +14 -0
  82. package/dist/src/tools/upload-artifact.d.ts.map +1 -0
  83. package/dist/src/tools/upload-artifact.js +166 -0
  84. package/dist/src/tools/upload-artifact.js.map +1 -0
  85. package/dist/src/utils/file-lock.d.ts +4 -0
  86. package/dist/src/utils/file-lock.d.ts.map +1 -0
  87. package/dist/src/utils/file-lock.js +43 -0
  88. package/dist/src/utils/file-lock.js.map +1 -0
  89. package/dist/src/utils/profile.d.ts +17 -0
  90. package/dist/src/utils/profile.d.ts.map +1 -0
  91. package/dist/src/utils/profile.js +26 -0
  92. package/dist/src/utils/profile.js.map +1 -0
  93. package/dist/src/utils/session.d.ts +3 -0
  94. package/dist/src/utils/session.d.ts.map +1 -0
  95. package/dist/src/utils/session.js +26 -0
  96. package/dist/src/utils/session.js.map +1 -0
  97. package/package.json +17 -5
  98. package/.env.example +0 -23
  99. package/channel/346/265/201/347/250/213/345/233/276.md +0 -477
  100. package/index.ts +0 -198
  101. package/src/channel/config.ts +0 -27
  102. package/src/channel/connection.ts +0 -341
  103. package/src/channel/dispatcher.ts +0 -374
  104. package/src/channel/index.ts +0 -173
  105. package/src/channel/logger.ts +0 -36
  106. package/src/channel/protocol.ts +0 -265
  107. package/src/channel/registrar.ts +0 -172
  108. package/src/channel/types.ts +0 -102
  109. package/src/core/index.ts +0 -13
  110. package/src/core/register-identity.ts +0 -326
  111. package/src/core/task-api.ts +0 -168
  112. package/src/core/urls.ts +0 -52
  113. package/src/prompt/job.md +0 -21
  114. package/src/tools/get-plugin-profile.ts +0 -152
  115. package/src/tools/grab-task.ts +0 -133
  116. package/src/tools/list-tasks.ts +0 -135
  117. package/src/tools/propose-registration.ts +0 -116
  118. package/src/tools/register-identity.ts +0 -121
  119. package/src/tools/submit-deliverable.ts +0 -268
  120. package/src/tools/upload-artifact.ts +0 -222
  121. package/src/utils/file-lock.ts +0 -43
  122. package/src/utils/profile.ts +0 -45
  123. package/src/utils/session.ts +0 -30
  124. package/tests/profile.test.ts +0 -70
  125. package/tests/session.test.ts +0 -53
  126. package/tsconfig.json +0 -49
  127. package/vitest.config.ts +0 -26
@@ -1,268 +0,0 @@
1
- import { createWriteStream, existsSync } from "node:fs";
2
- import { readdir, stat, readFile } from "node:fs/promises";
3
- import { basename, join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { randomBytes } from "node:crypto";
6
- import archiver from "archiver";
7
- import type {
8
- OpenClawPluginToolContext,
9
- OpenClawPluginToolFactory,
10
- } from "openclaw/plugin-sdk/plugin-entry";
11
- import { taskApiPostMultipart } from "../core/task-api.js";
12
-
13
- // ──────────────────────────────────────────────────────────────────────────────
14
- // 响应数据类型
15
- // ──────────────────────────────────────────────────────────────────────────────
16
-
17
- /** 上传交付物成功时 data 字段结构 */
18
- interface DeliverableData {
19
- result_zip_url?: string;
20
- result_description_url?: string;
21
- uploaded_at?: string;
22
- [key: string]: unknown;
23
- }
24
-
25
- // ──────────────────────────────────────────────────────────────────────────────
26
- // 工具参数 JSON Schema
27
- // ──────────────────────────────────────────────────────────────────────────────
28
-
29
- const SUBMIT_DELIVERABLE_PARAMS = {
30
- type: "object" as const,
31
- properties: {
32
- task_id: {
33
- type: "string",
34
- description: "已认领任务的 UUID(通过 insta_grab_task 抢单成功后获得)",
35
- },
36
- artifact_path: {
37
- type: "string",
38
- description:
39
- "产物路径(绝对路径)。" +
40
- "若为目录,将自动递归打包成 ZIP 后提交;" +
41
- "若为文件(含 .zip),直接上传,无需额外打包。",
42
- },
43
- result_description: {
44
- type: "string",
45
- description:
46
- "任务执行结果描述/报告(必填文字说明)。" +
47
- "请用简洁语言描述本次任务的执行结论、关键产出和注意事项," +
48
- "此文本将与产物文件一并提交给任务发布方。",
49
- },
50
- },
51
- required: ["task_id", "artifact_path", "result_description"] as string[],
52
- additionalProperties: false,
53
- };
54
-
55
- // ──────────────────────────────────────────────────────────────────────────────
56
- // 内部工具:将目录打包为 ZIP(写入临时文件)
57
- // ──────────────────────────────────────────────────────────────────────────────
58
-
59
- /**
60
- * 将指定目录递归打包成 ZIP 文件,写到系统临时目录。
61
- *
62
- * @param dirPath 需要打包的目录绝对路径
63
- * @returns 生成的临时 ZIP 文件绝对路径
64
- */
65
- async function packDirectoryToZip(dirPath: string): Promise<string> {
66
- const suffix = randomBytes(6).toString("hex");
67
- const zipName = `insta_delivery_${suffix}.zip`;
68
- const zipPath = join(tmpdir(), zipName);
69
-
70
- await new Promise<void>((resolve, reject) => {
71
- const output = createWriteStream(zipPath);
72
- const archive = archiver("zip", { zlib: { level: 6 } });
73
-
74
- output.on("close", resolve);
75
- archive.on("error", reject);
76
-
77
- archive.pipe(output);
78
-
79
- // 将整个目录添加进 zip(保留目录内部结构,根节点命名与目录名一致)
80
- archive.directory(dirPath, basename(dirPath));
81
-
82
- archive.finalize();
83
- });
84
-
85
- return zipPath;
86
- }
87
-
88
- // ──────────────────────────────────────────────────────────────────────────────
89
- // 工具:insta_submit_deliverable
90
- // ──────────────────────────────────────────────────────────────────────────────
91
-
92
- /**
93
- * 提交任务产物工具工厂函数。
94
- *
95
- * 功能:
96
- * 1. 校验 task_id / artifact_path / result_description
97
- * 2. 判断 artifact_path 是目录还是文件:
98
- * - 目录 → 调用 archiver 递归打包成 ZIP
99
- * - 文件 → 直接读取使用
100
- * 3. 通过 multipart/form-data POST 到 /bots/tasks/{task_id}/deliverable
101
- * 字段:result_description(文本)+ zip_file(文件)
102
- * 4. 服务端收到后自动将任务状态流转为 completed,无需手动调用状态更新接口
103
- *
104
- * 在 index.ts 中通过 api.registerTool(submitDeliverableToolFactory) 注册。
105
- */
106
- export const submitDeliverableToolFactory: OpenClawPluginToolFactory = (
107
- ctx: OpenClawPluginToolContext,
108
- ) => {
109
- return {
110
- name: "insta_submit_deliverable",
111
- label: "打包并提交引态任务产物",
112
- description: [
113
- "将本地产物文件或目录打包提交到引态(Instaon)任务系统,完成任务交付。",
114
- "提交成功后任务状态将自动流转为「已完成(completed)」,无需手动更新状态。",
115
- "参数说明:",
116
- " · task_id(必填):已认领任务的 UUID,可通过 insta_grab_task 抢单后获取。",
117
- " · artifact_path(必填):产物路径(绝对路径)。",
118
- " - 若指向目录:自动递归打包成 ZIP 后提交。",
119
- " - 若指向文件(含 .zip):直接上传,不额外打包。",
120
- " · result_description(必填):任务执行结果的文字说明/报告,",
121
- " 将与产物文件一并提交给任务发布方。",
122
- "返回说明:",
123
- " · ok=true:提交成功,data 中包含 result_zip_url 等链接信息。",
124
- " · ok=false:提交失败,error 中包含具体原因。",
125
- "认证方式:自动读取本地 profile.json 中的 app_key / app_secret。",
126
- ].join("\n"),
127
-
128
- parameters: SUBMIT_DELIVERABLE_PARAMS,
129
-
130
- execute: async (_toolCallId: string, params: unknown) => {
131
- let tempZipPath: string | null = null;
132
-
133
- try {
134
- const p = params as Record<string, unknown>;
135
- const taskId =
136
- typeof p["task_id"] === "string" ? p["task_id"].trim() : "";
137
- const artifactPath =
138
- typeof p["artifact_path"] === "string" ? p["artifact_path"].trim() : "";
139
- const resultDescription =
140
- typeof p["result_description"] === "string"
141
- ? p["result_description"].trim()
142
- : "";
143
-
144
- // ── Step 1: 参数校验 ─────────────────────────────────────────────────
145
- if (!taskId) {
146
- const err = { ok: false, error: "缺少必填参数:task_id(任务 UUID)" };
147
- return {
148
- content: [{ type: "text" as const, text: JSON.stringify(err) }],
149
- details: err,
150
- };
151
- }
152
-
153
- if (!artifactPath) {
154
- const err = { ok: false, error: "缺少必填参数:artifact_path(产物路径)" };
155
- return {
156
- content: [{ type: "text" as const, text: JSON.stringify(err) }],
157
- details: err,
158
- };
159
- }
160
-
161
- if (!resultDescription) {
162
- const err = {
163
- ok: false,
164
- error: "缺少必填参数:result_description(任务执行结果描述)",
165
- };
166
- return {
167
- content: [{ type: "text" as const, text: JSON.stringify(err) }],
168
- details: err,
169
- };
170
- }
171
-
172
- if (!existsSync(artifactPath)) {
173
- const err = { ok: false, error: `产物路径不存在:${artifactPath}` };
174
- return {
175
- content: [{ type: "text" as const, text: JSON.stringify(err) }],
176
- details: err,
177
- };
178
- }
179
-
180
- // ── Step 2: 判断路径类型,必要时打包 ZIP ─────────────────────────────
181
- const pathStat = await stat(artifactPath);
182
- let uploadFilePath: string;
183
- let packed = false;
184
-
185
- if (pathStat.isDirectory()) {
186
- // 目录 → 先检查是否为空目录
187
- const entries = await readdir(artifactPath);
188
- if (entries.length === 0) {
189
- const err = { ok: false, error: `产物目录为空,无法提交:${artifactPath}` };
190
- return {
191
- content: [{ type: "text" as const, text: JSON.stringify(err) }],
192
- details: err,
193
- };
194
- }
195
- // 打包成 zip
196
- tempZipPath = await packDirectoryToZip(artifactPath);
197
- uploadFilePath = tempZipPath;
198
- packed = true;
199
- } else {
200
- // 普通文件 → 直接上传
201
- uploadFilePath = artifactPath;
202
- }
203
-
204
- // ── Step 3: 读取文件,构造 multipart/form-data ────────────────────────
205
- const fileBuffer = await readFile(uploadFilePath);
206
- const filename = basename(uploadFilePath);
207
- const blob = new Blob([fileBuffer], { type: "application/zip" });
208
-
209
- const formData = new FormData();
210
- formData.append("result_description", resultDescription);
211
- formData.append("zip_file", blob, filename);
212
-
213
- // ── Step 4: 调用交付物上传接口 ────────────────────────────────────────
214
- const baseDir = ctx.agentDir ?? ctx.workspaceDir ?? process.cwd();
215
-
216
- const resp = await taskApiPostMultipart<DeliverableData>(
217
- baseDir,
218
- `/bots/tasks/${taskId}/deliverable`,
219
- formData,
220
- );
221
-
222
- if (resp.code !== 0) {
223
- const err = {
224
- ok: false,
225
- code: resp.code,
226
- error: resp.message ?? "提交交付物失败",
227
- };
228
- return {
229
- content: [{ type: "text" as const, text: JSON.stringify(err, null, 2) }],
230
- details: err,
231
- };
232
- }
233
-
234
- // ── Step 5: 返回成功结果 ──────────────────────────────────────────────
235
- const result = {
236
- ok: true,
237
- task_id: taskId,
238
- packed_from_directory: packed,
239
- uploaded_filename: filename,
240
- data: resp.data ?? {},
241
- message:
242
- "产物提交成功,任务状态已自动流转为「已完成」",
243
- };
244
-
245
- return {
246
- content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
247
- details: result,
248
- };
249
- } catch (e: unknown) {
250
- const message =
251
- e instanceof Error ? e.message : "提交产物过程中发生未知错误";
252
- const err = { ok: false, error: message };
253
- return {
254
- content: [{ type: "text" as const, text: message }],
255
- details: err,
256
- };
257
- } finally {
258
- // ── 清理临时 ZIP 文件 ─────────────────────────────────────────────────
259
- if (tempZipPath) {
260
- const { unlink } = await import("node:fs/promises");
261
- await unlink(tempZipPath).catch(() => {
262
- // 清理失败不影响主流程,静默处理
263
- });
264
- }
265
- }
266
- },
267
- };
268
- };
@@ -1,222 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import { basename } from "node:path";
4
- import type {
5
- OpenClawPluginToolContext,
6
- OpenClawPluginToolFactory,
7
- } from "openclaw/plugin-sdk/plugin-entry";
8
- import { getInstaUrl } from "../core/urls.js";
9
- import { readProfile } from "../utils/profile.js";
10
-
11
- // ──────────────────────────────────────────────────────────────────────────────
12
- // 响应结构定义
13
- // ──────────────────────────────────────────────────────────────────────────────
14
-
15
- /**
16
- * 制品上传并绑定接口的响应结构。
17
- * upload 子字段为必须;bind 子字段在传入 sessionId 时才会出现。
18
- */
19
- interface UploadAndBindResponse {
20
- upload: {
21
- url?: string;
22
- address?: string;
23
- status?: string;
24
- [key: string]: unknown;
25
- };
26
- bind?: {
27
- id?: string | number;
28
- session_id?: string;
29
- name?: string;
30
- address?: string;
31
- status?: string;
32
- [key: string]: unknown;
33
- };
34
- }
35
-
36
- /**
37
- * 将服务端原始响应解析为强类型结构。
38
- * 若必填字段缺失则返回 null,由调用方透传原始数据。
39
- */
40
- function parseUploadAndBindResponse(
41
- raw: unknown,
42
- ): UploadAndBindResponse | null {
43
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
44
- return null;
45
- }
46
- const obj = raw as Record<string, unknown>;
47
-
48
- // upload 字段为必须
49
- if (
50
- obj["upload"] === null ||
51
- typeof obj["upload"] !== "object" ||
52
- Array.isArray(obj["upload"])
53
- ) {
54
- return null;
55
- }
56
-
57
- return raw as UploadAndBindResponse;
58
- }
59
-
60
- // ──────────────────────────────────────────────────────────────────────────────
61
- // 工具参数 JSON Schema
62
- // ──────────────────────────────────────────────────────────────────────────────
63
-
64
- const UPLOAD_PARAMS = {
65
- type: "object" as const,
66
- properties: {
67
- filePath: {
68
- type: "string",
69
- description: "需要上传的本地文件的绝对路径",
70
- },
71
- sessionId: {
72
- type: "string",
73
- description:
74
- "当前雇佣会话的 sessionId。处于用户雇佣会话时必须传入," +
75
- "上传完成后会自动将文件与该会话关联。" +
76
- "如果系统提示词中包含【当前会话信息】,请将其中的 sessionId 值传入此参数。",
77
- },
78
- },
79
- required: ["filePath"] as string[],
80
- additionalProperties: false,
81
- };
82
-
83
- // ──────────────────────────────────────────────────────────────────────────────
84
- // 工具:insta_upload_artifact
85
- // ──────────────────────────────────────────────────────────────────────────────
86
-
87
- /**
88
- * 制品文件上传工具工厂函数。
89
- *
90
- * 功能:
91
- * 1. 校验本地文件是否存在
92
- * 2. 通过 multipart/form-data POST 到引态制品上传接口
93
- * 3. 可选传入 sessionId,将制品与雇佣会话自动绑定
94
- * 4. 解析并返回服务端响应(含下载地址等信息)
95
- *
96
- * 在 index.ts 中通过 api.registerTool(uploadArtifactToolFactory) 注册。
97
- */
98
- export const uploadArtifactToolFactory: OpenClawPluginToolFactory = (
99
- _ctx: OpenClawPluginToolContext,
100
- ) => {
101
- return {
102
- name: "insta_upload_artifact",
103
- label: "上传制品文件到引态平台",
104
- description: [
105
- "当用户雇佣你的时候(远程),用户无法操作你local环境,因此你需要把你的产物通过平台传递给用户。",
106
- "将本地制品文件上传到引态(Instaon)平台,并获取可访问的文件链接,可直接发送给用户。",
107
- "多个文件建议先压缩为 zip 后再使用此工具上传,不要上传无关文件。",
108
- "参数说明:",
109
- " · filePath(必填):需要上传的本地文件绝对路径。",
110
- " · sessionId(可选):当前雇佣会话的 sessionId。",
111
- " 若系统提示词中包含【当前会话信息】,请将其中的 sessionId 传入,",
112
- " 上传完成后文件将自动与该雇佣会话关联。",
113
- "返回数据:",
114
- " · upload.url 或 upload.address:文件的可访问地址。",
115
- " · bind(仅传入 sessionId 时出现):会话绑定结果。",
116
- ].join("\n"),
117
-
118
- parameters: UPLOAD_PARAMS,
119
-
120
- execute: async (_toolCallId: string, params: unknown) => {
121
- try {
122
- const p = params as Record<string, unknown>;
123
- const filePath =
124
- typeof p["filePath"] === "string" ? p["filePath"].trim() : "";
125
- const sessionId =
126
- typeof p["sessionId"] === "string" ? p["sessionId"].trim() : "";
127
-
128
- // ── Step 1: 参数校验 ────────────────────────────────────────────────
129
- if (!filePath) {
130
- const err = {
131
- ok: false,
132
- error: "缺少必填参数:filePath(文件绝对路径)",
133
- };
134
- return {
135
- content: [{ type: "text" as const, text: JSON.stringify(err) }],
136
- details: err,
137
- };
138
- }
139
-
140
- if (!existsSync(filePath)) {
141
- const err = { ok: false, error: `文件不存在:${filePath}` };
142
- return {
143
- content: [{ type: "text" as const, text: JSON.stringify(err) }],
144
- details: err,
145
- };
146
- }
147
-
148
- // ── Step 2: 读取文件,构造 multipart/form-data ─────────────────────
149
- const fileBuffer = await readFile(filePath);
150
- const filename = basename(filePath);
151
- const blob = new Blob([fileBuffer]);
152
-
153
- const formData = new FormData();
154
- formData.append("file", blob, filename);
155
- if (sessionId) {
156
- formData.append("sessionId", sessionId);
157
- }
158
-
159
- // ── Step 3: 读取认证凭据 ──────────────────────────────────────────
160
- const baseDir = _ctx.agentDir ?? _ctx.workspaceDir ?? process.cwd();
161
- const profile = await readProfile(baseDir);
162
- if (!profile?.app_key || !profile?.app_secret) {
163
- const err = {
164
- ok: false,
165
- error: "未找到有效的 app_key / app_secret,请先完成档案注册。",
166
- };
167
- return {
168
- content: [{ type: "text" as const, text: JSON.stringify(err) }],
169
- details: err,
170
- };
171
- }
172
-
173
- // ── Step 4: 调用制品上传并绑定接口 ────────────────────────────────
174
- const uploadUrl = getInstaUrl("artifactUploadAndBind");
175
- const response = await fetch(uploadUrl, {
176
- method: "POST",
177
- headers: {
178
- "x-app-key": profile.app_key,
179
- "x-app-secret": profile.app_secret,
180
- },
181
- body: formData,
182
- });
183
-
184
- if (!response.ok) {
185
- const errText = await response
186
- .text()
187
- .catch(() => `HTTP ${response.status}`);
188
- throw new Error(`上传失败:HTTP ${response.status} ${errText}`);
189
- }
190
-
191
- // ── Step 5: 解析并返回响应 ─────────────────────────────────────────
192
- const rawData: unknown = await response.json();
193
- const result = parseUploadAndBindResponse(rawData);
194
-
195
- if (result === null) {
196
- // 响应结构不符合预期,原样透传,不丢失信息
197
- return {
198
- content: [
199
- { type: "text" as const, text: JSON.stringify(rawData, null, 2) },
200
- ],
201
- details: rawData,
202
- };
203
- }
204
-
205
- return {
206
- content: [
207
- { type: "text" as const, text: JSON.stringify(result, null, 2) },
208
- ],
209
- details: result,
210
- };
211
- } catch (e: unknown) {
212
- const message =
213
- e instanceof Error ? e.message : "上传过程中发生未知错误";
214
- const err = { ok: false, error: message };
215
- return {
216
- content: [{ type: "text" as const, text: message }],
217
- details: err,
218
- };
219
- }
220
- },
221
- };
222
- };
@@ -1,43 +0,0 @@
1
- import { mkdir, open, stat, unlink } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
-
4
- const STALE_MS = 30_000;
5
-
6
- export async function ensureDir(filePath: string): Promise<void> {
7
- await mkdir(dirname(filePath), { recursive: true });
8
- }
9
-
10
- export async function acquireLock(baseDir: string, lockName: string): Promise<string> {
11
- const lockPath = join(baseDir, lockName);
12
- for (let i = 0; i < 100; i++) {
13
- try {
14
- const fd = await open(lockPath, "wx");
15
- await fd.write(Buffer.from(`${process.pid}\n`));
16
- await fd.close();
17
- return lockPath;
18
- } catch (err: unknown) {
19
- const code = (err as NodeJS.ErrnoException).code;
20
- if (code !== "EEXIST") throw err;
21
-
22
- try {
23
- const s = await stat(lockPath);
24
- if (Date.now() - s.mtimeMs > STALE_MS) {
25
- await unlink(lockPath);
26
- continue;
27
- }
28
- } catch {
29
- // lock removed between checks — retry
30
- }
31
- await new Promise((r) => setTimeout(r, 50 + Math.random() * 50));
32
- }
33
- }
34
- throw new Error(`Failed to acquire lock: ${lockPath}`);
35
- }
36
-
37
- export async function releaseLock(lockPath: string): Promise<void> {
38
- try {
39
- await unlink(lockPath);
40
- } catch {
41
- // already released
42
- }
43
- }
@@ -1,45 +0,0 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { acquireLock, ensureDir, releaseLock } from "./file-lock.js";
4
-
5
- export type Profile = {
6
- app_key: string;
7
- app_secret: string;
8
- name: string;
9
- description: string;
10
- claw_id: string;
11
- hourly_rate: number;
12
- enable_commissioned: boolean;
13
- enable_hired: boolean;
14
- scene_tags: string[];
15
- custom_tags: string[];
16
- avatar: string;
17
- instance_type: number;
18
- };
19
-
20
- const PROFILE_RELATIVE_PATH = ".insta/config/profile.json";
21
-
22
- export async function readProfile(baseDir: string): Promise<Profile | null> {
23
- const filePath = join(baseDir, PROFILE_RELATIVE_PATH);
24
- try {
25
- const raw = await readFile(filePath, "utf-8");
26
- return JSON.parse(raw) as Profile;
27
- } catch {
28
- return null;
29
- }
30
- }
31
-
32
- export async function writeProfile(
33
- baseDir: string,
34
- profile: Profile,
35
- ): Promise<void> {
36
- const filePath = join(baseDir, PROFILE_RELATIVE_PATH);
37
- await ensureDir(filePath);
38
-
39
- const lockPath = await acquireLock(dirname(filePath), ".profile.lock");
40
- try {
41
- await writeFile(filePath, JSON.stringify(profile, null, 2), "utf-8");
42
- } finally {
43
- await releaseLock(lockPath);
44
- }
45
- }
@@ -1,30 +0,0 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { acquireLock, ensureDir, releaseLock } from "./file-lock.js";
4
-
5
- const SESSION_RELATIVE_PATH = ".insta/.activate_session";
6
-
7
- export async function readSession(baseDir: string): Promise<string | null> {
8
- const filePath = join(baseDir, SESSION_RELATIVE_PATH);
9
- try {
10
- const raw = await readFile(filePath, "utf-8");
11
- return raw.trim() || null;
12
- } catch {
13
- return null;
14
- }
15
- }
16
-
17
- export async function writeSession(
18
- baseDir: string,
19
- sessionId: string,
20
- ): Promise<void> {
21
- const filePath = join(baseDir, SESSION_RELATIVE_PATH);
22
- await ensureDir(filePath);
23
-
24
- const lockPath = await acquireLock(join(baseDir, ".insta"), ".session.lock");
25
- try {
26
- await writeFile(filePath, sessionId, "utf-8");
27
- } finally {
28
- await releaseLock(lockPath);
29
- }
30
- }
@@ -1,70 +0,0 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { describe, it, expect, afterEach, beforeEach } from "vitest";
5
- import { readProfile, writeProfile, type Profile } from "../src/utils/profile.js";
6
-
7
- let workDir: string;
8
-
9
- beforeEach(async () => {
10
- workDir = await mkdtemp(join(tmpdir(), "profile-test-"));
11
- });
12
-
13
- afterEach(async () => {
14
- await rm(workDir, { recursive: true, force: true });
15
- });
16
-
17
- const sampleProfile: Profile = {
18
- app_key: "key_123",
19
- app_secret: "secret_abc",
20
- name: "Test App",
21
- description: "A test profile",
22
- claw_id: "claw_001",
23
- hourly_rate: 50.0,
24
- enable_commissioned: true,
25
- enable_hired: true,
26
- scene_tags: ["social", "automation"],
27
- custom_tags: ["beta"],
28
- avatar: "https://example.com/avatar.png",
29
- instance_type: 0,
30
- };
31
-
32
- describe("readProfile", () => {
33
- it("returns null when file does not exist", async () => {
34
- const result = await readProfile(workDir);
35
- expect(result).toBeNull();
36
- });
37
-
38
- it("returns profile after write", async () => {
39
- await writeProfile(workDir, sampleProfile);
40
- const result = await readProfile(workDir);
41
- expect(result).toEqual(sampleProfile);
42
- });
43
- });
44
-
45
- describe("writeProfile", () => {
46
- it("creates directory structure and writes file", async () => {
47
- await writeProfile(workDir, sampleProfile);
48
- const result = await readProfile(workDir);
49
- expect(result).not.toBeNull();
50
- expect(result!.name).toBe("Test App");
51
- });
52
-
53
- it("overwrites existing profile", async () => {
54
- await writeProfile(workDir, sampleProfile);
55
- const updated = { ...sampleProfile, name: "Updated" };
56
- await writeProfile(workDir, updated);
57
- const result = await readProfile(workDir);
58
- expect(result!.name).toBe("Updated");
59
- });
60
-
61
- it("handles concurrent writes without corruption", async () => {
62
- const writes = Array.from({ length: 20 }, (_, i) =>
63
- writeProfile(workDir, { ...sampleProfile, hourly_rate: i }),
64
- );
65
- await Promise.all(writes);
66
- const result = await readProfile(workDir);
67
- expect(result).not.toBeNull();
68
- expect(typeof result!.hourly_rate).toBe("number");
69
- });
70
- });