@sjhmars/happy-bridge 0.3.7 → 0.3.8
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 -1
- package/lib/client.js +11 -11
- package/lib/index.js +65 -6
- package/lib/types/catalogs.d.ts +12 -0
- package/lib/types/grant.d.ts +10 -0
- package/lib/types/inbound.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- 新建:只能选已经在网页登记过的工作区。不会在磁盘上 mkdir。App 里「new worktree」会失败(故意不暴露 bash)。
|
|
22
22
|
- 发文字:已注册 `/命令` 走命令通道;否则当聊天。`/skill名` 仍当普通消息,由 harness 注入。
|
|
23
23
|
- 发图片:相册/拍照/粘贴后,会随下一条文字(也可以只发图)进同一场电脑对话,模型能看见图。`.java` / `.txt` 等普通文件会先下载到工作区 `happy-inbox/`,再走电脑原来的 `read` 工具读内容(和你在仓库里打开一个文件一样)。PDF、Word 这类不是纯文本的文件,电脑的 `read` 会拒绝,插件也不会自己拆文档。远程档至少「能聊」。输入框左边应有图片按钮;发出后电脑网页会出现图片,手机会转圈等回复。电脑网页贴的图也会按同一套加密上传推到手机。
|
|
24
|
-
- 工具批准、计划审阅、选择题:和网页黄框/提问框赛跑,谁先点谁算。手机点「始终允许」只在这次网页进程里记住。选择题走 Happy
|
|
24
|
+
- 工具批准、计划审阅、选择题:和网页黄框/提问框赛跑,谁先点谁算。手机点「始终允许」只在这次网页进程里记住。选择题走 Happy 表单通道,首题末尾带一行提示「自定义回答:直接在下方聊天输入框输入并发送」——想选就点选项提交,想自由回答就直接打字发送,那条文本会作为答案并回显到卡片。**网页上填自定义答案或点选项,也会把同一份答案写回手机卡片**,不会再把手机表单标成「取消」(取消会让 App 走进输入框自定义流程,把网页模型栏锁回目录第一项 DeepSeek V4)。旧版 App 没有这条通道,见 `questionChannel`。提问还在等的时候,直接用输入框打字也等于自己填;审阅计划时打字会关掉审阅,这句话当普通聊天发出去。批完/拒完的卡片会停在「已批准/已拒绝」状态,按钮失效。远程档至少「能批」才能从手机点。
|
|
25
25
|
- 电脑正在生成时,手机可以点停止(Happy App 的 Stop)。远程档至少「能聊」。只看档点了不会停。
|
|
26
26
|
- 助手回复按整段推到手机(生成中会转圈)。思考收成一行 **Think · 摘要** 的 Note 卡片,点进去看全文。工具用 Happy 认识的名字(Grep / Read / Bash),描述跟网页那一行摘要一样。Happy 没有网页同款图标。
|
|
27
27
|
|
package/lib/client.js
CHANGED
|
@@ -17,21 +17,21 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
document.head.appendChild(tag);
|
|
18
18
|
}
|
|
19
19
|
var HappyBridgeCard_module_css_default = {
|
|
20
|
-
"headText": "l9rBEq_headText",
|
|
21
|
-
"name": "l9rBEq_name",
|
|
22
|
-
"card": "l9rBEq_card",
|
|
23
|
-
"status": "l9rBEq_status",
|
|
24
20
|
"description": "l9rBEq_description",
|
|
25
|
-
"
|
|
21
|
+
"status": "l9rBEq_status",
|
|
22
|
+
"label": "l9rBEq_label",
|
|
26
23
|
"error": "l9rBEq_error",
|
|
27
|
-
"
|
|
24
|
+
"row": "l9rBEq_row",
|
|
25
|
+
"chevron": "l9rBEq_chevron",
|
|
28
26
|
"button": "l9rBEq_button",
|
|
29
|
-
"qr": "l9rBEq_qr",
|
|
30
|
-
"body": "l9rBEq_body",
|
|
31
27
|
"checkbox": "l9rBEq_checkbox",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
28
|
+
"card": "l9rBEq_card",
|
|
29
|
+
"header": "l9rBEq_header",
|
|
30
|
+
"headText": "l9rBEq_headText",
|
|
31
|
+
"name": "l9rBEq_name",
|
|
32
|
+
"qr": "l9rBEq_qr",
|
|
33
|
+
"select": "l9rBEq_select",
|
|
34
|
+
"body": "l9rBEq_body"
|
|
35
35
|
};
|
|
36
36
|
//#endregion
|
|
37
37
|
//#region src/client/HappyBridgeCard.tsx
|
package/lib/index.js
CHANGED
|
@@ -247,7 +247,8 @@ async function buildSessionMetadata(ctx, source, machineId, title, selection, _g
|
|
|
247
247
|
value: name,
|
|
248
248
|
description: name === "danger-full-access" ? "不限制文件,且不再询问批准" : "只能改当前工作区,危险工具要批准"
|
|
249
249
|
}));
|
|
250
|
-
const selected =
|
|
250
|
+
const selected = resolvePublishedSelection(models, selection);
|
|
251
|
+
const publishedModels = selected === void 0 || models.some((model) => model.providerId === selected.row.providerId && model.id === selected.row.id) ? models : [selected.row, ...models];
|
|
251
252
|
const thoughtLevels = selected?.row.effortOptions;
|
|
252
253
|
const thought = selected === void 0 ? void 0 : selected.effort ?? selected.row.defaultThinkingLevel;
|
|
253
254
|
return {
|
|
@@ -271,7 +272,7 @@ async function buildSessionMetadata(ctx, source, machineId, title, selection, _g
|
|
|
271
272
|
happyToolsDir: VIRTUAL_HOME,
|
|
272
273
|
slashCommands,
|
|
273
274
|
skills,
|
|
274
|
-
models,
|
|
275
|
+
models: publishedModels,
|
|
275
276
|
operatingModes,
|
|
276
277
|
client: {
|
|
277
278
|
id: "rig",
|
|
@@ -329,17 +330,47 @@ async function buildSessionMetadata(ctx, source, machineId, title, selection, _g
|
|
|
329
330
|
}
|
|
330
331
|
};
|
|
331
332
|
}
|
|
332
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Pick the Happy catalog row that matches Host selection.
|
|
335
|
+
* A known Host pick that is missing from the listed rows stays that pick
|
|
336
|
+
* (synthetic row) instead of falling back to `models[0]` (usually DeepSeek V4).
|
|
337
|
+
* @param models - rows advertised to the App.
|
|
338
|
+
* @param selection - current Host provider/model/effort, when known.
|
|
339
|
+
* @returns the row to publish, plus effort when the Host named one.
|
|
340
|
+
*/
|
|
341
|
+
function resolvePublishedSelection(models, selection) {
|
|
333
342
|
if (selection !== void 0) {
|
|
334
343
|
const row = models.find((model) => model.providerId === selection.provider && model.id === selection.model) ?? models.find((model) => model.code === `${selection.provider}/${selection.model}`);
|
|
335
344
|
if (row !== void 0) return {
|
|
336
345
|
row,
|
|
337
346
|
...selection.reasoningEffort === void 0 ? {} : { effort: selection.reasoningEffort }
|
|
338
347
|
};
|
|
348
|
+
return {
|
|
349
|
+
row: syntheticHappyRow(selection),
|
|
350
|
+
...selection.reasoningEffort === void 0 ? {} : { effort: selection.reasoningEffort }
|
|
351
|
+
};
|
|
339
352
|
}
|
|
340
353
|
const first = models[0];
|
|
341
354
|
return first === void 0 ? void 0 : { row: first };
|
|
342
355
|
}
|
|
356
|
+
function syntheticHappyRow(selection) {
|
|
357
|
+
const effort = selection.reasoningEffort;
|
|
358
|
+
return {
|
|
359
|
+
code: `${selection.provider}/${selection.model}`,
|
|
360
|
+
value: selection.model,
|
|
361
|
+
id: selection.model,
|
|
362
|
+
name: selection.model,
|
|
363
|
+
providerId: selection.provider,
|
|
364
|
+
providerKind: "custom",
|
|
365
|
+
providerName: selection.provider,
|
|
366
|
+
thinkingLevels: effort === void 0 ? [] : [effort],
|
|
367
|
+
effortOptions: effort === void 0 ? [] : [{
|
|
368
|
+
code: effort,
|
|
369
|
+
value: effort
|
|
370
|
+
}],
|
|
371
|
+
...effort === void 0 ? {} : { defaultThinkingLevel: effort }
|
|
372
|
+
};
|
|
373
|
+
}
|
|
343
374
|
function listCommands(ctx, agent) {
|
|
344
375
|
const commands = ctx.get("commands");
|
|
345
376
|
if (commands === void 0) return [];
|
|
@@ -1105,6 +1136,22 @@ function sameCatalogPick(previous, next) {
|
|
|
1105
1136
|
if (previous === void 0) return false;
|
|
1106
1137
|
return catalogPickKey(previous) === catalogPickKey(next);
|
|
1107
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Whether inbound `meta.model` is the pick we last published.
|
|
1141
|
+
* The App often echoes only `currentModelCode` (bare id) on a chat line,
|
|
1142
|
+
* while we publish `provider:id`; treating that as a user switch would
|
|
1143
|
+
* lock the web composer onto the catalog default (DeepSeek V4).
|
|
1144
|
+
* @param published - last pick we wrote, if any.
|
|
1145
|
+
* @param inbound - model code from the inbound message.
|
|
1146
|
+
* @returns true when the inbound model is our own echo.
|
|
1147
|
+
*/
|
|
1148
|
+
function isPublishedModelEcho(published, inbound) {
|
|
1149
|
+
if (published?.model === void 0) return false;
|
|
1150
|
+
if (sameCatalogPick({ model: published.model }, { model: inbound })) return true;
|
|
1151
|
+
const publishedSplit = splitModelCode(published.model);
|
|
1152
|
+
const inboundSplit = splitModelCode(inbound);
|
|
1153
|
+
return inboundSplit.provider === "" && inboundSplit.model === publishedSplit.model;
|
|
1154
|
+
}
|
|
1108
1155
|
function catalogPickKey(pick) {
|
|
1109
1156
|
const split = pick.model === void 0 ? void 0 : splitModelCode(pick.model);
|
|
1110
1157
|
return `${split === void 0 ? "" : split.provider === "" ? split.model : `${split.provider}/${split.model}`}\0${pick.effort === void 0 ? "" : pick.effort === null ? "null" : pick.effort}`;
|
|
@@ -1767,6 +1814,18 @@ function mergeCustomAnswers(questions, text, deferred) {
|
|
|
1767
1814
|
});
|
|
1768
1815
|
}
|
|
1769
1816
|
/**
|
|
1817
|
+
* Translate a web-side harness answer batch into Happy communications answers
|
|
1818
|
+
* so the phone card shows the same choice instead of a cancelled form.
|
|
1819
|
+
* @param answers - harness answer rows from the web composer.
|
|
1820
|
+
* @returns answers keyed by question id.
|
|
1821
|
+
*/
|
|
1822
|
+
function communicationAnswersFromWeb(answers) {
|
|
1823
|
+
return Object.fromEntries(answers.map((row) => [row.id, {
|
|
1824
|
+
options: row.selected,
|
|
1825
|
+
...row.custom === void 0 || row.custom === "" ? {} : { custom: row.custom }
|
|
1826
|
+
}]));
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1770
1829
|
* Translate Happy communications answers (`{ [question id]: { options, custom } }`)
|
|
1771
1830
|
* into harness answer rows. Options become `selected`; free text becomes `custom`.
|
|
1772
1831
|
* @param answers - communication RPC answers keyed by question id.
|
|
@@ -3789,7 +3848,7 @@ var HappyBridge = class {
|
|
|
3789
3848
|
const effort = messageEffort(meta);
|
|
3790
3849
|
const permissionMode = meta.permissionMode;
|
|
3791
3850
|
const published = this.lastPublished.get(agent.id);
|
|
3792
|
-
const modelIsEcho = model !== void 0 &&
|
|
3851
|
+
const modelIsEcho = model !== void 0 && isPublishedModelEcho(published, model);
|
|
3793
3852
|
const effortIsEcho = effort !== void 0 && published?.effort != null && effort === published.effort;
|
|
3794
3853
|
if (model !== void 0 && !modelIsEcho) {
|
|
3795
3854
|
if (!grantAtLeast(this.config.remoteGrant, "full")) link.socket.sendText("service", "改模型需要远程档「完整」,这条消息仍会发出。");
|
|
@@ -4253,8 +4312,8 @@ var HappyBridge = class {
|
|
|
4253
4312
|
const pending = link.pendingHuman;
|
|
4254
4313
|
if (pending !== void 0) {
|
|
4255
4314
|
delete link.pendingHuman;
|
|
4256
|
-
if (pending.kind === "ask" && pending.communication !== void 0) this.completeCommunication(link, pending.id, pending.communication, "
|
|
4257
|
-
else this.clearRequest(link, pending.id, "
|
|
4315
|
+
if (pending.kind === "ask" && pending.communication !== void 0) this.completeCommunication(link, pending.id, pending.communication, "answered", communicationAnswersFromWeb(answer.answers));
|
|
4316
|
+
else this.clearRequest(link, pending.id, "approved", pending.request);
|
|
4258
4317
|
link.socket.sendToolEnd(pending.id);
|
|
4259
4318
|
}
|
|
4260
4319
|
return answer;
|
package/lib/types/catalogs.d.ts
CHANGED
|
@@ -117,4 +117,16 @@ export declare function buildSessionMetadata(ctx: Context, source: {
|
|
|
117
117
|
}[];
|
|
118
118
|
agent?: Agent;
|
|
119
119
|
}, machineId: string, title: string, selection: HappySelection | undefined, _grant: RemoteGrant): Promise<SessionMetadata>;
|
|
120
|
+
/**
|
|
121
|
+
* Pick the Happy catalog row that matches Host selection.
|
|
122
|
+
* A known Host pick that is missing from the listed rows stays that pick
|
|
123
|
+
* (synthetic row) instead of falling back to `models[0]` (usually DeepSeek V4).
|
|
124
|
+
* @param models - rows advertised to the App.
|
|
125
|
+
* @param selection - current Host provider/model/effort, when known.
|
|
126
|
+
* @returns the row to publish, plus effort when the Host named one.
|
|
127
|
+
*/
|
|
128
|
+
export declare function resolvePublishedSelection(models: HappyModelRow[], selection: HappySelection | undefined): {
|
|
129
|
+
row: HappyModelRow;
|
|
130
|
+
effort?: string;
|
|
131
|
+
} | undefined;
|
|
120
132
|
//# sourceMappingURL=catalogs.d.ts.map
|
package/lib/types/grant.d.ts
CHANGED
|
@@ -84,4 +84,14 @@ export interface CatalogModelPick {
|
|
|
84
84
|
* @returns true when the inbound pick is our own echo.
|
|
85
85
|
*/
|
|
86
86
|
export declare function sameCatalogPick(previous: CatalogModelPick | undefined, next: CatalogModelPick): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Whether inbound `meta.model` is the pick we last published.
|
|
89
|
+
* The App often echoes only `currentModelCode` (bare id) on a chat line,
|
|
90
|
+
* while we publish `provider:id`; treating that as a user switch would
|
|
91
|
+
* lock the web composer onto the catalog default (DeepSeek V4).
|
|
92
|
+
* @param published - last pick we wrote, if any.
|
|
93
|
+
* @param inbound - model code from the inbound message.
|
|
94
|
+
* @returns true when the inbound model is our own echo.
|
|
95
|
+
*/
|
|
96
|
+
export declare function isPublishedModelEcho(published: CatalogModelPick | undefined, inbound: string): boolean;
|
|
87
97
|
//# sourceMappingURL=grant.d.ts.map
|
package/lib/types/inbound.d.ts
CHANGED
|
@@ -110,6 +110,20 @@ export declare function mergeCustomAnswers(questions: readonly {
|
|
|
110
110
|
selected: string[];
|
|
111
111
|
custom?: string;
|
|
112
112
|
}[];
|
|
113
|
+
/**
|
|
114
|
+
* Translate a web-side harness answer batch into Happy communications answers
|
|
115
|
+
* so the phone card shows the same choice instead of a cancelled form.
|
|
116
|
+
* @param answers - harness answer rows from the web composer.
|
|
117
|
+
* @returns answers keyed by question id.
|
|
118
|
+
*/
|
|
119
|
+
export declare function communicationAnswersFromWeb(answers: readonly {
|
|
120
|
+
id: string;
|
|
121
|
+
selected: string[];
|
|
122
|
+
custom?: string;
|
|
123
|
+
}[]): Record<string, {
|
|
124
|
+
options: string[];
|
|
125
|
+
custom?: string;
|
|
126
|
+
}>;
|
|
113
127
|
/**
|
|
114
128
|
* Translate Happy communications answers (`{ [question id]: { options, custom } }`)
|
|
115
129
|
* into harness answer rows. Options become `selected`; free text becomes `custom`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjhmars/happy-bridge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Remote-control bridge: pair a running dsh web/desktop client with the Happy mobile app by QR code, then remote-control the same harness sessions from the phone (send messages, read replies, approve tool calls, switch models)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|