@sleep2agi/dsh-anet 0.1.0-preview.30 → 0.1.0-preview.32
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 +1 -0
- package/README.zh.md +1 -0
- package/package.json +1 -1
- package/src/ocr.swift +17 -0
- package/src/plugin.mjs +89 -7
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 (up to 5 images, 10 MB each): vision models receive image blocks, text-only models receive macOS Vision OCR text instead. 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 下载(最多 5 张、单张 ≤10MB):视觉模型收到 image block,文本模型(如 DeepSeek chat-completions)收到 macOS Vision OCR 文字。其他类型附件暂不处理。
|
|
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.
|
|
3
|
+
"version": "0.1.0-preview.32",
|
|
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/ocr.swift
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Vision
|
|
3
|
+
import AppKit
|
|
4
|
+
|
|
5
|
+
let path = CommandLine.arguments[1]
|
|
6
|
+
guard let image = NSImage(contentsOfFile: path),
|
|
7
|
+
let cg = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
|
8
|
+
exit(2)
|
|
9
|
+
}
|
|
10
|
+
let request = VNRecognizeTextRequest()
|
|
11
|
+
request.recognitionLevel = .accurate
|
|
12
|
+
request.usesLanguageCorrection = true
|
|
13
|
+
request.recognitionLanguages = ["zh-Hans", "en-US"]
|
|
14
|
+
let handler = VNImageRequestHandler(cgImage: cg, options: [:])
|
|
15
|
+
try handler.perform([request])
|
|
16
|
+
let lines = (request.results ?? []).compactMap { $0.topCandidates(1).first?.string }
|
|
17
|
+
print(lines.joined(separator: "\n"))
|
package/src/plugin.mjs
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// Each node has its own name, hub, token, project directory and dedicated
|
|
3
3
|
// session; tasks for a node run in that node's session so context is preserved.
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { accessSync, chmodSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
10
|
import { basename, dirname, join } from "node:path";
|
|
8
11
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
9
12
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
@@ -17,11 +20,12 @@ import { findNodeBySession, loadNodes, newSessionId, normalizeNode, registryPath
|
|
|
17
20
|
import { startConfigServer } from "./ui-server.mjs";
|
|
18
21
|
|
|
19
22
|
export { summarizeTurn } from "./turn.mjs";
|
|
23
|
+
export { attachmentCandidates, collectAttachments };
|
|
20
24
|
|
|
21
25
|
export const name = "anet";
|
|
22
|
-
export const inject = ["agents", "agentDefaultModel", "sessions", "tools"];
|
|
26
|
+
export const inject = ["agents", "agentDefaultModel", "sessions", "tools", "attachments"];
|
|
23
27
|
|
|
24
|
-
const VERSION = "dsh-anet@0.1.0-preview.
|
|
28
|
+
const VERSION = "dsh-anet@0.1.0-preview.32";
|
|
25
29
|
|
|
26
30
|
function agentSetup(ctx) {
|
|
27
31
|
const selection = ctx.agentDefaultModel.currentSelection();
|
|
@@ -48,10 +52,88 @@ async function ensureAgent(ctx, node) {
|
|
|
48
52
|
return handle.agent;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
const IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
56
|
+
const MAX_IMAGES = 5;
|
|
57
|
+
const MIME_BY_EXT = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
|
|
58
|
+
|
|
59
|
+
const runFile = promisify(execFile);
|
|
60
|
+
const ocrSource = fileURLToPath(new URL("./ocr.swift", import.meta.url));
|
|
61
|
+
|
|
62
|
+
async function ocrImage(path) {
|
|
63
|
+
const bin = join(homedir(), ".anet", "dsh", "anet-ocr");
|
|
64
|
+
try { accessSync(bin); } catch {
|
|
65
|
+
mkdirSync(dirname(bin), { recursive: true, mode: 0o700 });
|
|
66
|
+
await runFile("swiftc", ["-O", ocrSource, "-o", bin], { timeout: 120_000 });
|
|
67
|
+
chmodSync(bin, 0o700);
|
|
68
|
+
}
|
|
69
|
+
const { stdout } = await runFile(bin, [path], { timeout: 60_000, maxBuffer: 2 * 1024 * 1024 });
|
|
70
|
+
return String(stdout).trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function modelReadsImages(selection) {
|
|
74
|
+
const id = `${selection?.provider ?? ""} ${selection?.model ?? ""}`.toLowerCase();
|
|
75
|
+
if (id.includes("deepseek")) return /vl|vision|multimodal/.test(id);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function attachmentCandidates(task) {
|
|
80
|
+
const candidates = [];
|
|
81
|
+
for (const item of task?.meta?.attachments ?? task?.attachments ?? []) {
|
|
82
|
+
if (item && typeof item === "object") candidates.push({ id: item.file_id ?? item.id, url: item.url, name: item.name, mime: item.mime });
|
|
83
|
+
}
|
|
84
|
+
const text = String(task?.content ?? "");
|
|
85
|
+
const re = /附件\s+(\S+?)(([^)]+))[\s\S]*?API:\s*GET\s+(\/api\/files\/[A-Za-z0-9_-]+)/gu;
|
|
86
|
+
for (const match of text.matchAll(re)) candidates.push({ name: match[1], mime: match[2], url: match[3] });
|
|
87
|
+
return candidates;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function collectAttachments(ctx, node, task, options = {}) {
|
|
91
|
+
const blocks = []; const notes = []; let text = "";
|
|
92
|
+
const inline = options.inlineImages !== false;
|
|
93
|
+
const read = options.ocr ?? ocrImage;
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (const candidate of attachmentCandidates(task)) {
|
|
96
|
+
if (count >= MAX_IMAGES) break;
|
|
97
|
+
const mime = String(candidate.mime ?? MIME_BY_EXT[String(candidate.name ?? "").split(".").pop()?.toLowerCase()] ?? "");
|
|
98
|
+
if (!mime.startsWith("image/")) continue;
|
|
99
|
+
const url = candidate.url
|
|
100
|
+
? (String(candidate.url).startsWith("http") ? String(candidate.url) : `${node.hub}${String(candidate.url).startsWith("/") ? "" : "/"}${candidate.url}`)
|
|
101
|
+
: (candidate.id ? `${node.hub}/api/files/${encodeURIComponent(candidate.id)}` : "");
|
|
102
|
+
if (!url) continue;
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${node.token}` }, signal: AbortSignal.timeout(30_000) });
|
|
105
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
106
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
107
|
+
if (data.byteLength === 0) throw new Error("empty file");
|
|
108
|
+
if (data.byteLength > IMAGE_MAX_BYTES) throw new Error("image larger than 10 MB");
|
|
109
|
+
const tmp = join(tmpdir(), `anet-attach-${randomUUID().slice(0, 8)}.${(candidate.name ?? "img").split(".").pop() || "png"}`);
|
|
110
|
+
writeFileSync(tmp, data, { mode: 0o600 });
|
|
111
|
+
let recognized = "";
|
|
112
|
+
try { recognized = await read(tmp); } catch { /* OCR unavailable */ }
|
|
113
|
+
try { unlinkSync(tmp); } catch { /* best effort */ }
|
|
114
|
+
if (inline) {
|
|
115
|
+
const attachment = await ctx.attachments.saveImage({ data, mediaType: mime, ...(candidate.name ? { name: candidate.name } : {}) });
|
|
116
|
+
blocks.push({ type: "image", attachment });
|
|
117
|
+
}
|
|
118
|
+
if (recognized) text += `${text ? "\n" : ""}[图片文字 ${candidate.name ?? "image"}]\n${recognized}`;
|
|
119
|
+
notes.push(recognized ? `[附件 ${candidate.name ?? "image"}:已提取文字]` : `[附件 ${candidate.name ?? "image"}${inline ? "" : ":模型不支持图片,未提取到文字"}]`);
|
|
120
|
+
count++;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
notes.push(`[附件 ${candidate.name ?? candidate.id ?? url} 下载失败: ${error?.message ?? error}]`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { blocks, notes, text };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runNodeTurn(ctx, node, prompt, task) {
|
|
129
|
+
const selection = ctx.agentDefaultModel.currentSelection();
|
|
130
|
+
const attachments = await collectAttachments(ctx, node, task, { inlineImages: modelReadsImages(selection) });
|
|
52
131
|
const agent = await ensureAgent(ctx, node);
|
|
53
132
|
const firstSeq = agent.session.seq;
|
|
54
|
-
|
|
133
|
+
const content = [{ type: "text", text: prompt }, ...attachments.blocks];
|
|
134
|
+
if (attachments.text) content.push({ type: "text", text: attachments.text });
|
|
135
|
+
if (attachments.notes.length > 0) content.push({ type: "text", text: attachments.notes.join("\n") });
|
|
136
|
+
agent.followup(createUserMessage({ content, source: { kind: "user" } }));
|
|
55
137
|
await agent.whenIdle();
|
|
56
138
|
await ctx.sessions.flush(agent.session);
|
|
57
139
|
const { text, reason, errors } = summarizeTurn(agent.session, firstSeq);
|
|
@@ -97,7 +179,7 @@ export function apply(ctx, config) {
|
|
|
97
179
|
const loop = createCommhubNode({
|
|
98
180
|
client, alias: node.name, ledger: createLedger(ledgerPath), log,
|
|
99
181
|
version: VERSION,
|
|
100
|
-
runTurn: (prompt) => runNodeTurn(ctx, node, prompt),
|
|
182
|
+
runTurn: (prompt, task) => runNodeTurn(ctx, node, prompt, task),
|
|
101
183
|
});
|
|
102
184
|
runtime.loop = loop;
|
|
103
185
|
runtimes.set(node.id, runtime);
|