@sleep2agi/dsh-anet 0.1.0-preview.29 → 0.1.0-preview.31

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 CHANGED
@@ -53,6 +53,7 @@ The user token is never printed or persisted; `--no-verify` skips the round-trip
53
53
  - If the plugin process dies mid-turn, the task is answered `failed` with "please resend" on the next start rather than being re-run silently.
54
54
  - Replies over the hub's 10 000-character limit are truncated with a visible note.
55
55
  - Informational (non-task) inbox messages are acknowledged but not shown to the agent in this version.
56
+ - Image attachments on tasks are downloaded with the node token and passed to the agent as image blocks (up to 5 images, 10 MB each); other attachment types are not processed yet.
56
57
 
57
58
  ## Tests
58
59
 
package/README.zh.md CHANGED
@@ -51,6 +51,7 @@ ANET_USER_TOKEN=utok_... node scripts/connect.mjs \
51
51
  - 插件进程在一轮中途死掉,下次启动时对该任务回 `failed` 并提示「请重发」,而不是悄悄重跑。
52
52
  - 超过 hub 1 万字上限的回件会截断并附说明。
53
53
  - 本版对「消息」类(非任务)收件只确认,不交给 agent。
54
+ - 任务里的图片附件会用节点 token 下载并作为 image block 交给 agent(最多 5 张、单张 ≤10MB);其他类型附件暂不处理。
54
55
 
55
56
  ## 测试
56
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/dsh-anet",
3
- "version": "0.1.0-preview.29",
3
+ "version": "0.1.0-preview.31",
4
4
  "description": "DSH (DeepSeek Harness) plugin: expose one or more DSH sessions as Agent Network (anet.sh / CommHub) nodes.",
5
5
  "type": "module",
6
6
  "main": "./src/plugin.mjs",
package/src/client.js CHANGED
@@ -88,6 +88,17 @@ window.__ModuleLoader__.load({
88
88
  return `${title} · ${sessionWhen(session.createdAt)} · ${baseName(session.cwd)}`;
89
89
  }
90
90
 
91
+ function AnetMark(props) {
92
+ const size = props.size ?? 20;
93
+ return h("svg", { width: size, height: size, viewBox: "0 0 64 64", "aria-hidden": "true", style: { display: "block", flex: "none" } },
94
+ h("rect", { width: 64, height: 64, rx: 14, fill: "#0f172a" }),
95
+ h("circle", { cx: 20, cy: 22, r: 6, fill: "#38bdf8" }),
96
+ h("circle", { cx: 44, cy: 22, r: 6, fill: "#a78bfa" }),
97
+ h("circle", { cx: 32, cy: 44, r: 6, fill: "#f472b6" }),
98
+ h("path", { d: "M20 22 L32 44 L44 22 Z", stroke: "#e2e8f0", strokeWidth: 2, fill: "none" }),
99
+ );
100
+ }
101
+
91
102
  function Field(props) {
92
103
  return h("div", { style: props.style ?? null },
93
104
  h("label", { style: labelStyle, htmlFor: props.id }, props.label),
@@ -185,7 +196,7 @@ window.__ModuleLoader__.load({
185
196
  fontSize: props.wide ? 13 : 11, fontWeight: 600, cursor: "pointer",
186
197
  },
187
198
  },
188
- h("span", { style: { width: 16, height: 16, borderRadius: 5, background: T.brand, color: T.onBrand, fontSize: 10, fontWeight: 700, display: "inline-flex", alignItems: "center", justifyContent: "center" } }, "A"),
199
+ h(AnetMark, { size: 18 }),
189
200
  props.wide ? "ANet" : null,
190
201
  ),
191
202
  open ? h(AnetOverlay, { t, onClose: () => openStore.set(false) }) : null,
@@ -209,7 +220,7 @@ window.__ModuleLoader__.load({
209
220
  },
210
221
  },
211
222
  h("div", { style: { display: "flex", alignItems: "center", gap: 12, padding: "18px 20px", borderBottom: `1px solid ${T.border}` } },
212
- h("div", { style: { width: 34, height: 34, borderRadius: 10, background: T.brand, color: T.onBrand, fontSize: 16, fontWeight: 800, display: "flex", alignItems: "center", justifyContent: "center" } }, "A"),
223
+ h(AnetMark, { size: 34 }),
213
224
  h("div", { style: { flex: 1, minWidth: 0 } },
214
225
  h("div", { style: { fontSize: 16, fontWeight: 700, color: T.text } }, t("title")),
215
226
  h("div", { style: { fontSize: 12, color: T.muted, marginTop: 2 } }, t("subtitle")),
package/src/plugin.mjs CHANGED
@@ -17,11 +17,12 @@ import { findNodeBySession, loadNodes, newSessionId, normalizeNode, registryPath
17
17
  import { startConfigServer } from "./ui-server.mjs";
18
18
 
19
19
  export { summarizeTurn } from "./turn.mjs";
20
+ export { attachmentCandidates, collectAttachments };
20
21
 
21
22
  export const name = "anet";
22
- export const inject = ["agents", "agentDefaultModel", "sessions", "tools"];
23
+ export const inject = ["agents", "agentDefaultModel", "sessions", "tools", "attachments"];
23
24
 
24
- const VERSION = "dsh-anet@0.1.0-preview.29";
25
+ const VERSION = "dsh-anet@0.1.0-preview.31";
25
26
 
26
27
  function agentSetup(ctx) {
27
28
  const selection = ctx.agentDefaultModel.currentSelection();
@@ -48,10 +49,56 @@ async function ensureAgent(ctx, node) {
48
49
  return handle.agent;
49
50
  }
50
51
 
51
- async function runNodeTurn(ctx, node, prompt) {
52
+ const IMAGE_MAX_BYTES = 10 * 1024 * 1024;
53
+ const MAX_IMAGES = 5;
54
+ const MIME_BY_EXT = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
55
+
56
+ function attachmentCandidates(task) {
57
+ const candidates = [];
58
+ for (const item of task?.meta?.attachments ?? task?.attachments ?? []) {
59
+ if (item && typeof item === "object") candidates.push({ id: item.file_id ?? item.id, url: item.url, name: item.name, mime: item.mime });
60
+ }
61
+ const text = String(task?.content ?? "");
62
+ const re = /附件\s+(\S+?)(([^)]+))[\s\S]*?API:\s*GET\s+(\/api\/files\/[A-Za-z0-9_-]+)/gu;
63
+ for (const match of text.matchAll(re)) candidates.push({ name: match[1], mime: match[2], url: match[3] });
64
+ return candidates;
65
+ }
66
+
67
+ async function collectAttachments(ctx, node, task) {
68
+ const blocks = []; const notes = [];
69
+ let count = 0;
70
+ for (const candidate of attachmentCandidates(task)) {
71
+ if (count >= MAX_IMAGES) break;
72
+ const mime = String(candidate.mime ?? MIME_BY_EXT[String(candidate.name ?? "").split(".").pop()?.toLowerCase()] ?? "");
73
+ if (!mime.startsWith("image/")) continue;
74
+ const url = candidate.url
75
+ ? (String(candidate.url).startsWith("http") ? String(candidate.url) : `${node.hub}${String(candidate.url).startsWith("/") ? "" : "/"}${candidate.url}`)
76
+ : (candidate.id ? `${node.hub}/api/files/${encodeURIComponent(candidate.id)}` : "");
77
+ if (!url) continue;
78
+ try {
79
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${node.token}` }, signal: AbortSignal.timeout(30_000) });
80
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
81
+ const data = new Uint8Array(await res.arrayBuffer());
82
+ if (data.byteLength === 0) throw new Error("empty file");
83
+ if (data.byteLength > IMAGE_MAX_BYTES) throw new Error("image larger than 10 MB");
84
+ const attachment = await ctx.attachments.saveImage({ data, mediaType: mime, ...(candidate.name ? { name: candidate.name } : {}) });
85
+ blocks.push({ type: "image", attachment });
86
+ notes.push(`[附件 ${candidate.name ?? "image"}]`);
87
+ count++;
88
+ } catch (error) {
89
+ notes.push(`[附件 ${candidate.name ?? candidate.id ?? url} 下载失败: ${error?.message ?? error}]`);
90
+ }
91
+ }
92
+ return { blocks, notes };
93
+ }
94
+
95
+ async function runNodeTurn(ctx, node, prompt, task) {
96
+ const attachments = await collectAttachments(ctx, node, task);
52
97
  const agent = await ensureAgent(ctx, node);
53
98
  const firstSeq = agent.session.seq;
54
- agent.followup(createUserMessage({ content: [{ type: "text", text: prompt }], source: { kind: "user" } }));
99
+ const content = [{ type: "text", text: prompt }, ...attachments.blocks];
100
+ if (attachments.notes.length > 0) content.push({ type: "text", text: attachments.notes.join("\n") });
101
+ agent.followup(createUserMessage({ content, source: { kind: "user" } }));
55
102
  await agent.whenIdle();
56
103
  await ctx.sessions.flush(agent.session);
57
104
  const { text, reason, errors } = summarizeTurn(agent.session, firstSeq);
@@ -97,7 +144,7 @@ export function apply(ctx, config) {
97
144
  const loop = createCommhubNode({
98
145
  client, alias: node.name, ledger: createLedger(ledgerPath), log,
99
146
  version: VERSION,
100
- runTurn: (prompt) => runNodeTurn(ctx, node, prompt),
147
+ runTurn: (prompt, task) => runNodeTurn(ctx, node, prompt, task),
101
148
  });
102
149
  runtime.loop = loop;
103
150
  runtimes.set(node.id, runtime);