@anarkhgatsby/deepseek-harness-channel-imessage 0.1.1
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/LICENSE +21 -0
- package/README.md +58 -0
- package/client.js +22 -0
- package/cordis.patch.yml +6 -0
- package/index.js +216 -0
- package/lib/adapters/local.mjs +131 -0
- package/lib/config.mjs +81 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mapan0424
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# harness-channel-imessage
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness 的本机 iMessage 通道插件。插件只通过 macOS 的 `Messages.app` 和本地 `chat.db` 收发消息,不使用 `imsg` CLI、Photon 或任何云中继。
|
|
4
|
+
|
|
5
|
+
## 隐私设计
|
|
6
|
+
|
|
7
|
+
- 消息数据不经过第三方云服务。
|
|
8
|
+
- 入站消息从 `~/Library/Messages/chat.db` 只读轮询。
|
|
9
|
+
- 出站消息通过 macOS `/usr/bin/osascript` 控制 `Messages.app` 发送。
|
|
10
|
+
- 不需要安装额外的 CLI,也不需要配置云端 API Key。
|
|
11
|
+
- 需要为 DeepSeek Harness 授予“完全磁盘访问”和“自动化 → 信息”权限。
|
|
12
|
+
|
|
13
|
+
## 架构
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Harness Agent
|
|
17
|
+
│
|
|
18
|
+
▼
|
|
19
|
+
Cordis 通道插件(host: index.js)
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
GatewayCore(来自 @anarkhgatsby/deepseek-harness-core)
|
|
23
|
+
│ 统一消息总线:路由 / 去重 / 投递 / 流式回复
|
|
24
|
+
▼
|
|
25
|
+
LocalAdapter
|
|
26
|
+
├── chat.db:读取入站消息
|
|
27
|
+
└── Messages.app:发送出站消息
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 配置
|
|
31
|
+
|
|
32
|
+
插件保留 `local` 模式作为兼容标识,但当前没有模式选择项。配置页面只展示:
|
|
33
|
+
|
|
34
|
+
- `chatDb`:Messages 数据库路径,默认 `~/Library/Messages/chat.db`
|
|
35
|
+
- `defaultWorkspace`:默认工作空间路径
|
|
36
|
+
- 自动回复
|
|
37
|
+
- 流式回复
|
|
38
|
+
|
|
39
|
+
收到消息后,插件会根据 sender 路由到对应工作空间,并由 Harness Agent 自动回复。
|
|
40
|
+
|
|
41
|
+
## 代码结构
|
|
42
|
+
|
|
43
|
+
- `index.js` — 注册 `imessage` settings namespace、启动本地网关和 `message_imessage` 工具。
|
|
44
|
+
- `client.js` — 导出本地模式的客户端元数据。
|
|
45
|
+
- `lib/config.mjs` — 本地模式配置 schema 与归一化逻辑。
|
|
46
|
+
- `lib/adapters/local.mjs` — `chat.db` 监听与 `Messages.app` AppleScript 发送。
|
|
47
|
+
- `cordis.patch.yml` — Cordis 补丁入口。
|
|
48
|
+
|
|
49
|
+
## 开发
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node --check index.js
|
|
53
|
+
node --check client.js
|
|
54
|
+
node --check lib/config.mjs
|
|
55
|
+
node --check lib/adapters/local.mjs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
License: MIT。
|
package/client.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
if (typeof window !== "undefined" && window.__ModuleLoader__) {
|
|
2
|
+
window.__ModuleLoader__.load({
|
|
3
|
+
id: "@anarkhgatsby/deepseek-harness-channel-imessage",
|
|
4
|
+
factory: (require) => {
|
|
5
|
+
const module = { exports: {} };
|
|
6
|
+
const exports = module.exports;
|
|
7
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
8
|
+
|
|
9
|
+
exports.IMESSAGE_GATEWAY = "imessageGateway";
|
|
10
|
+
exports.MODES = ["local"];
|
|
11
|
+
exports.modeMeta = {
|
|
12
|
+
local: {
|
|
13
|
+
label: "本机 Messages.app",
|
|
14
|
+
description: "通过本机 chat.db 与 Messages.app 直接收发 iMessage。数据不经过第三方云服务。",
|
|
15
|
+
fields: ["chatDb", "defaultWorkspace"],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return module.exports;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.js — harness-channel-imessage host 入口(薄壳,托管消息总线)
|
|
3
|
+
*
|
|
4
|
+
* iMessage 仅使用本机 local 传输:通过 chat.db 监听,并由 Messages.app 发送消息。
|
|
5
|
+
* 消息总线(GatewayCore)与 logger 均从共享包 `@anarkhgatsby/deepseek-harness-core` 导入,
|
|
6
|
+
* 本插件只做"平台门面":注册 imessage settings namespace、选适配器、注册 message 工具。
|
|
7
|
+
*
|
|
8
|
+
* 数据落盘:$DSH_HOME/settings.yaml 的 `imessage` 段 + $DSH_HOME/imessage-gateway-state.json
|
|
9
|
+
* (sender→session 映射)。client 通过 Typert remote `imessageGateway` 读写。
|
|
10
|
+
*/
|
|
11
|
+
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
12
|
+
import z from "@deepseek-ai/schemastery";
|
|
13
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { GatewayCore, createChannelLogger } from "@anarkhgatsby/deepseek-harness-core";
|
|
17
|
+
import { LocalAdapter } from "./lib/adapters/local.mjs";
|
|
18
|
+
import { MODES, DEFAULT_MODE, normalizeSettings } from "./lib/config.mjs";
|
|
19
|
+
|
|
20
|
+
export const name = "harness-channel-imessage";
|
|
21
|
+
|
|
22
|
+
// 框架依赖:typert/settings(配置 remote)+ agents/agentPresets/workspaceRegistry/
|
|
23
|
+
// sessionPersistence/sessionTitle/tools(网关投递 + message 工具)。
|
|
24
|
+
export const inject = [
|
|
25
|
+
"typert",
|
|
26
|
+
"settings",
|
|
27
|
+
"agents",
|
|
28
|
+
"agentDefaultModel",
|
|
29
|
+
"agentPresets",
|
|
30
|
+
"sessions",
|
|
31
|
+
"workspaceRegistry",
|
|
32
|
+
"sessionPersistence",
|
|
33
|
+
"sessionTitle",
|
|
34
|
+
"tools",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** 插件自身 schema:settingsPath(settings.yaml)+ statePath(sender→session 映射)。 */
|
|
38
|
+
export const Config = z.object({
|
|
39
|
+
settingsPath: z.string().default(join(homedir(), ".dsh", "settings.yaml")),
|
|
40
|
+
statePath: z.string().default(join(homedir(), ".dsh", "imessage-gateway-state.json")),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** `imessage` settings namespace schema(路由 + 通用开关 + 各通道专属配置)。 */
|
|
44
|
+
const GatewaySchema = z.object({
|
|
45
|
+
routes: z.dict(z.string()),
|
|
46
|
+
mode: z.string(), // 兼容旧配置,当前仅支持 local
|
|
47
|
+
chatDb: z.string(),
|
|
48
|
+
defaultWorkspace: z.string(),
|
|
49
|
+
autoReply: z.boolean(),
|
|
50
|
+
streamReplies: z.boolean(),
|
|
51
|
+
toolCallReplies: z.boolean(),
|
|
52
|
+
stepTimeoutSec: z.number(),
|
|
53
|
+
allowlist: z.array(z.string()),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── Typert wire schemas ───────────────────────────────────────────────────
|
|
57
|
+
function parseObj() {
|
|
58
|
+
return {
|
|
59
|
+
parse(value) {
|
|
60
|
+
if (typeof value !== "object" || value === null) throw new Error("expected object");
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const getResultSchema = parseObj();
|
|
66
|
+
const setPayloadSchema = parseObj();
|
|
67
|
+
const setResultSchema = parseObj();
|
|
68
|
+
|
|
69
|
+
/** Typert MANIFEST:注册给 API gateway 的远程方法。 */
|
|
70
|
+
const MANIFEST = {
|
|
71
|
+
package: "harness-channel-imessage",
|
|
72
|
+
face: "host",
|
|
73
|
+
schemas: [],
|
|
74
|
+
invocations: [
|
|
75
|
+
{
|
|
76
|
+
id: "harness-channel-imessage#imessageGateway/getConfig",
|
|
77
|
+
service: "imessageGateway",
|
|
78
|
+
namespace: "imessageGateway",
|
|
79
|
+
method: "getConfig",
|
|
80
|
+
invocation: { kind: "direct" },
|
|
81
|
+
parameters: [],
|
|
82
|
+
result: { mode: "strict", typeSymbol: "harness-channel-imessage#GatewayConfig", schema: getResultSchema },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "harness-channel-imessage#imessageGateway/setConfig",
|
|
86
|
+
service: "imessageGateway",
|
|
87
|
+
namespace: "imessageGateway",
|
|
88
|
+
method: "setConfig",
|
|
89
|
+
invocation: { kind: "direct" },
|
|
90
|
+
parameters: [
|
|
91
|
+
{
|
|
92
|
+
name: "payload",
|
|
93
|
+
wire: "payload",
|
|
94
|
+
source: "json",
|
|
95
|
+
codec: { mode: "strict", typeSymbol: "harness-channel-imessage#SetPayload", schema: setPayloadSchema },
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
result: { mode: "strict", typeSymbol: "harness-channel-imessage#SetResult", schema: setResultSchema },
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
model: { services: [], events: [], objects: [] },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
class GatewayService extends TypertRemoteService {
|
|
105
|
+
constructor(ctx, scope, adapter) {
|
|
106
|
+
super(ctx, "imessageGateway");
|
|
107
|
+
this.scope = scope;
|
|
108
|
+
this.adapter = adapter;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getConfig() {
|
|
112
|
+
const snap = this.scope.get();
|
|
113
|
+
return normalizeSettings(snap);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async setConfig(payload) {
|
|
117
|
+
const current = this.scope.get() ?? {};
|
|
118
|
+
// 用 replace(整体替换)避免 update 深合并导致删路由不生效。
|
|
119
|
+
const section = normalizeSettings({ ...current, ...payload });
|
|
120
|
+
await this.scope.replace(section);
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function apply(ctx, config) {
|
|
126
|
+
const scope = ctx.settings.register("imessage", GatewaySchema, {
|
|
127
|
+
base: {
|
|
128
|
+
routes: {},
|
|
129
|
+
mode: DEFAULT_MODE,
|
|
130
|
+
chatDb: join(homedir(), "Library/Messages/chat.db"),
|
|
131
|
+
defaultWorkspace: join(homedir(), "dsh", "default"),
|
|
132
|
+
autoReply: true,
|
|
133
|
+
streamReplies: true,
|
|
134
|
+
toolCallReplies: true,
|
|
135
|
+
stepTimeoutSec: 0,
|
|
136
|
+
allowlist: [],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const log = createChannelLogger("im", ctx.logger);
|
|
141
|
+
|
|
142
|
+
// iMessage 仅允许本机 local 适配器;热更新时由 scope.watch 重载配置。
|
|
143
|
+
const resolveAdapter = () => {
|
|
144
|
+
const getConfig = () => normalizeSettings(scope.get());
|
|
145
|
+
return new LocalAdapter({ getConfig, log });
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const adapter = resolveAdapter();
|
|
149
|
+
const core = new GatewayCore({
|
|
150
|
+
tag: "im",
|
|
151
|
+
adapter,
|
|
152
|
+
agents: ctx.get("agents"),
|
|
153
|
+
defaultModel: ctx.get("agentDefaultModel"),
|
|
154
|
+
sessions: ctx.get("sessions"),
|
|
155
|
+
agentPresets: ctx.get("agentPresets"),
|
|
156
|
+
workspaceRegistry: ctx.get("workspaceRegistry"),
|
|
157
|
+
sessionPersistence: ctx.get("sessionPersistence"),
|
|
158
|
+
sessionTitle: ctx.get("sessionTitle"),
|
|
159
|
+
log,
|
|
160
|
+
statePath: config.statePath,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 配置 remote(配置页读写)
|
|
164
|
+
new GatewayService(ctx, scope, adapter);
|
|
165
|
+
ctx.effect(() => ctx.typert.register(MANIFEST), "harness-channel-imessage: typert manifest");
|
|
166
|
+
|
|
167
|
+
// 启动网关监听
|
|
168
|
+
ctx.on("dispose", () => core.stopListener());
|
|
169
|
+
core.startListener().then(() => log.info("网关监听已启动")).catch((e) => log.error(`启动监听失败 ${e instanceof Error ? e.message : e}`));
|
|
170
|
+
|
|
171
|
+
// 配置热更新:配置页保存后立即推给运行中的网关
|
|
172
|
+
scope.watch((next) => {
|
|
173
|
+
const normalized = normalizeSettings(next);
|
|
174
|
+
core.applyConfig(normalized);
|
|
175
|
+
log.info(`配置热更新: mode=${normalized.mode} routes=${Object.keys(core.routes).length}条 autoReply=${core.autoReply}`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 注册全局 `message_imessage` 工具
|
|
179
|
+
const messageTool = defineTool({
|
|
180
|
+
name: "message_imessage",
|
|
181
|
+
description: "通过本机 Messages.app 的 iMessage 通道向联系人发送一条文本消息。用于主动通知/提醒用户。",
|
|
182
|
+
parameters: {
|
|
183
|
+
action: { type: "string", required: true, description: "操作类型,目前仅支持 send" },
|
|
184
|
+
channel: { type: "string", required: true, description: "发送渠道:imessage/local" },
|
|
185
|
+
target: { type: "string", required: true, description: "目标 handle(号码如 +8613800000000 或 email)" },
|
|
186
|
+
message: { type: "string", required: true, description: "要发送的文本内容" },
|
|
187
|
+
},
|
|
188
|
+
output: {
|
|
189
|
+
schema: { type: "object", additionalProperties: false, properties: { ok: { type: "boolean", required: true } } },
|
|
190
|
+
render(args, value) {
|
|
191
|
+
const target = (typeof args === "object" && args !== null && typeof args.target === "string") ? args.target : "?";
|
|
192
|
+
return [{ type: "text", text: value.ok ? `已发送到 ${target}` : "发送失败" }];
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
async execute(args) {
|
|
196
|
+
let a = args;
|
|
197
|
+
if (typeof args === "string") { try { a = JSON.parse(args); } catch { return { ok: false }; } }
|
|
198
|
+
const action = a?.action ?? "send";
|
|
199
|
+
const target = a?.target;
|
|
200
|
+
const text = a?.message;
|
|
201
|
+
if (action !== "send" || !target || !text) return { ok: false };
|
|
202
|
+
try {
|
|
203
|
+
await core.send(target, text);
|
|
204
|
+
log.info(`message 工具已发送到 ${target}: ${String(text).slice(0, 40)}`);
|
|
205
|
+
return { ok: true };
|
|
206
|
+
} catch (e) {
|
|
207
|
+
log.error(`message 工具发送失败: ${e instanceof Error ? e.message : e}`);
|
|
208
|
+
return { ok: false };
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
ctx.tools.register(messageTool);
|
|
213
|
+
log.info("已注册全局 message 工具(iMessage 发送)");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { MODES, DEFAULT_MODE };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native macOS iMessage transport.
|
|
3
|
+
*
|
|
4
|
+
* Incoming messages are read from the local Messages database; outgoing text
|
|
5
|
+
* is sent by Messages.app through AppleScript. No imsg CLI or third-party
|
|
6
|
+
* relay is required.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { DatabaseSync } from "node:sqlite";
|
|
11
|
+
|
|
12
|
+
const POLL_INTERVAL_MS = 1000;
|
|
13
|
+
|
|
14
|
+
const SEND_SCRIPT = `on run argv
|
|
15
|
+
set targetHandle to item 1 of argv
|
|
16
|
+
set outgoingText to item 2 of argv
|
|
17
|
+
tell application "Messages"
|
|
18
|
+
set targetService to first service whose service type = iMessage
|
|
19
|
+
set targetBuddy to buddy targetHandle of targetService
|
|
20
|
+
send outgoingText to targetBuddy
|
|
21
|
+
end tell
|
|
22
|
+
end run`;
|
|
23
|
+
|
|
24
|
+
function friendlyError(error, action) {
|
|
25
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
26
|
+
return `${action}失败:${detail}。请在“系统设置 → 隐私与安全性”中为 DeepSeek Harness 开启完全磁盘访问;首次发送时允许其自动化控制“信息”。`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class LocalAdapter {
|
|
30
|
+
constructor({ getConfig, log = console }) {
|
|
31
|
+
this.getConfig = getConfig;
|
|
32
|
+
this.log = log;
|
|
33
|
+
this.db = null;
|
|
34
|
+
this.timer = null;
|
|
35
|
+
this.lastRowId = 0;
|
|
36
|
+
this.handler = null;
|
|
37
|
+
this.polling = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async start(handler) {
|
|
41
|
+
this.handler = handler;
|
|
42
|
+
this._openDatabase();
|
|
43
|
+
this.lastRowId = this._latestRowId();
|
|
44
|
+
this.timer = setInterval(() => void this._poll(), POLL_INTERVAL_MS);
|
|
45
|
+
this.log.info?.(`[local] 正在监听 Messages chat.db(从第 ${this.lastRowId} 条之后开始)`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_openDatabase() {
|
|
49
|
+
const chatDb = this.getConfig().chatDb;
|
|
50
|
+
if (!chatDb || !existsSync(chatDb)) {
|
|
51
|
+
throw new Error(`找不到 chat.db:${chatDb || "未配置路径"}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
this.db = new DatabaseSync(chatDb, { open: true, readOnly: true });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(friendlyError(error, "打开 Messages 数据库"));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_latestRowId() {
|
|
61
|
+
const row = this.db.prepare("SELECT COALESCE(MAX(ROWID), 0) AS id FROM message").get();
|
|
62
|
+
return Number(row?.id || 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async _poll() {
|
|
66
|
+
if (this.polling || !this.db) return;
|
|
67
|
+
this.polling = true;
|
|
68
|
+
try {
|
|
69
|
+
const rows = this.db
|
|
70
|
+
.prepare(`
|
|
71
|
+
SELECT m.ROWID AS id, m.guid AS guid, m.text AS text,
|
|
72
|
+
m.is_from_me AS is_from_me, h.id AS sender
|
|
73
|
+
FROM message AS m
|
|
74
|
+
LEFT JOIN handle AS h ON h.ROWID = m.handle_id
|
|
75
|
+
WHERE m.ROWID > ? AND COALESCE(m.is_from_me, 0) = 0
|
|
76
|
+
ORDER BY m.ROWID ASC
|
|
77
|
+
`)
|
|
78
|
+
.all(this.lastRowId);
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
this.lastRowId = Math.max(this.lastRowId, Number(row.id || 0));
|
|
81
|
+
const sender = typeof row.sender === "string" ? row.sender.trim() : "";
|
|
82
|
+
const text = typeof row.text === "string" ? row.text.trim() : "";
|
|
83
|
+
if (!sender || !text) continue;
|
|
84
|
+
await this.handler?.({
|
|
85
|
+
sender,
|
|
86
|
+
text,
|
|
87
|
+
images: [],
|
|
88
|
+
raw: { id: row.id, guid: row.guid },
|
|
89
|
+
dedupeId: row.guid || String(row.id),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.log.error?.(`[local] ${friendlyError(error, "读取 Messages 消息")}`);
|
|
94
|
+
} finally {
|
|
95
|
+
this.polling = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async send(to, text) {
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
const child = spawn("/usr/bin/osascript", ["-e", SEND_SCRIPT, to, text], {
|
|
102
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
103
|
+
});
|
|
104
|
+
let stderr = "";
|
|
105
|
+
child.stderr?.setEncoding("utf8");
|
|
106
|
+
child.stderr?.on("data", (chunk) => (stderr += chunk));
|
|
107
|
+
child.on("error", reject);
|
|
108
|
+
child.on("close", (code) => {
|
|
109
|
+
if (code === 0) resolve();
|
|
110
|
+
else reject(new Error(stderr.trim() || `osascript 退出 code=${code}`));
|
|
111
|
+
});
|
|
112
|
+
}).catch((error) => {
|
|
113
|
+
throw new Error(friendlyError(error, "通过 Messages.app 发送消息"));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async setTyping() {
|
|
118
|
+
// Messages.app does not expose a stable typing indicator API.
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async stop() {
|
|
122
|
+
if (this.timer) clearInterval(this.timer);
|
|
123
|
+
this.timer = null;
|
|
124
|
+
this.db?.close();
|
|
125
|
+
this.db = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
describe() {
|
|
129
|
+
return "macOS 本地通道(chat.db + Messages.app)";
|
|
130
|
+
}
|
|
131
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.mjs — harness-imessage 统一配置 schema 与校验
|
|
3
|
+
*
|
|
4
|
+
* iMessage 仅使用本机 `local` 传输:chat.db 监听 + Messages.app(无 CLI、无云中继)。
|
|
5
|
+
* 配置保留 mode 字段用于兼容已有 settings,但唯一允许的值是 "local"。
|
|
6
|
+
*/
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
/** 导出供 client 渲染的模式列表与默认。 */
|
|
11
|
+
export const MODES = ["local"];
|
|
12
|
+
export const DEFAULT_MODE = "local";
|
|
13
|
+
|
|
14
|
+
/** 当前运行环境默认 chat.db 路径(网关以 root 运行时可用环境变量覆盖)。 */
|
|
15
|
+
export function defaults() {
|
|
16
|
+
return {
|
|
17
|
+
mode: DEFAULT_MODE,
|
|
18
|
+
// local(本机)
|
|
19
|
+
chatDb: process.env.IMSG_CHAT_DB || join(homedir(), "Library/Messages/chat.db"),
|
|
20
|
+
defaultWorkspace: process.env.IMSG_DEFAULT_WORKSPACE || join(homedir(), "dsh", "default"),
|
|
21
|
+
// 通用
|
|
22
|
+
autoReply: true,
|
|
23
|
+
streamReplies: true,
|
|
24
|
+
toolCallReplies: true,
|
|
25
|
+
stepTimeoutSec: 0,
|
|
26
|
+
allowlist: [],
|
|
27
|
+
routes: {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 对一份 settings 快照做宽松校验/归一化。
|
|
33
|
+
* 返回归一化对象;非法值回退默认,不抛错(前端已做基本校验兜底)。
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeSettings(input) {
|
|
36
|
+
const base = defaults();
|
|
37
|
+
if (!input || typeof input !== "object") return base;
|
|
38
|
+
|
|
39
|
+
const pick = (key, fallback) =>
|
|
40
|
+
typeof input[key] === "string" && input[key].trim() ? input[key].trim() : fallback;
|
|
41
|
+
const bool = (key, fallback) =>
|
|
42
|
+
typeof input[key] === "boolean" ? input[key] : fallback;
|
|
43
|
+
const num = (key, fallback) => {
|
|
44
|
+
const n = Number(input[key]);
|
|
45
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const mode = MODES.includes(input.mode) ? input.mode : base.mode;
|
|
49
|
+
|
|
50
|
+
const out = {
|
|
51
|
+
...base,
|
|
52
|
+
mode,
|
|
53
|
+
chatDb: pick("chatDb", base.chatDb),
|
|
54
|
+
defaultWorkspace: pick("defaultWorkspace", base.defaultWorkspace),
|
|
55
|
+
autoReply: bool("autoReply", base.autoReply),
|
|
56
|
+
streamReplies: bool("streamReplies", base.streamReplies),
|
|
57
|
+
toolCallReplies: bool("toolCallReplies", base.toolCallReplies),
|
|
58
|
+
stepTimeoutSec: num("stepTimeoutSec", base.stepTimeoutSec),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// routes: handle → workspace 路径
|
|
62
|
+
if (input.routes && typeof input.routes === "object") {
|
|
63
|
+
const routes = {};
|
|
64
|
+
for (const [k, v] of Object.entries(input.routes)) {
|
|
65
|
+
if (typeof v === "string" && v.trim()) {
|
|
66
|
+
const key = typeof k === "string" ? k.trim() : String(k);
|
|
67
|
+
if (key) routes[key] = v.trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
out.routes = routes;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// allowlist: 允许的用户/号码列表(字符串数组)
|
|
74
|
+
if (Array.isArray(input.allowlist)) {
|
|
75
|
+
out.allowlist = input.allowlist
|
|
76
|
+
.map((x) => (typeof x === "string" ? x.trim() : ""))
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anarkhgatsby/deepseek-harness-channel-imessage",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Channel plugin for DeepSeek Harness: local-only iMessage transport through Messages.app and chat.db. Thin facade on @anarkhgatsby/deepseek-harness-core.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/mapan0424/deepseek-harness-desktop.git"
|
|
15
|
+
},
|
|
16
|
+
"dsh": {
|
|
17
|
+
"bundle": {
|
|
18
|
+
"patch": "./cordis.patch.yml"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"client.js",
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"lib/config.mjs",
|
|
26
|
+
"lib/adapters/local.mjs",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@anarkhgatsby/deepseek-harness-core": "^0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
39
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.6",
|
|
40
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
41
|
+
"@deepseek-ai/schemastery": ">=3.18.0"
|
|
42
|
+
}
|
|
43
|
+
}
|