@sjhmars/happy-bridge 0.1.0
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 +90 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +305 -0
- package/lib/index.js +4548 -0
- package/lib/invariant.js +15 -0
- package/lib/types/archive-sync.d.ts +38 -0
- package/lib/types/attachments.d.ts +19 -0
- package/lib/types/bridge.d.ts +227 -0
- package/lib/types/bytes.d.ts +16 -0
- package/lib/types/catalogs.d.ts +119 -0
- package/lib/types/client/HappyBridgeCard.d.ts +41 -0
- package/lib/types/client/index.d.ts +16 -0
- package/lib/types/client/locales.d.ts +34 -0
- package/lib/types/credentials.d.ts +59 -0
- package/lib/types/encryption.d.ts +104 -0
- package/lib/types/grant.d.ts +87 -0
- package/lib/types/happy-version.d.ts +11 -0
- package/lib/types/history.d.ts +184 -0
- package/lib/types/http.d.ts +108 -0
- package/lib/types/inbound.d.ts +88 -0
- package/lib/types/inbox.d.ts +39 -0
- package/lib/types/index.d.ts +24 -0
- package/lib/types/invariant.d.ts +11 -0
- package/lib/types/machine.d.ts +40 -0
- package/lib/types/pairing.d.ts +23 -0
- package/lib/types/paths.d.ts +53 -0
- package/lib/types/remote.d.ts +34 -0
- package/lib/types/rpc.d.ts +22 -0
- package/lib/types/session-socket.d.ts +145 -0
- package/lib/types/types.d.ts +104 -0
- package/package.json +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @sjhmars/happy-bridge
|
|
2
|
+
|
|
3
|
+
电脑照常跑 `dsh web` / 桌面客户端。装上这个插件并扫码后,手机 [Happy App](https://github.com/slopus/happy) 遥控的是**同一套对话**(发消息、看回复、点批准),不是另开一个 agent。
|
|
4
|
+
|
|
5
|
+
不安装 Happy CLI。不要用 `happy acp -- dsh acp`。本插件不改 DeepSeek Harness 源码,也不改 Happy 官方仓库。
|
|
6
|
+
|
|
7
|
+
## 你会看见什么
|
|
8
|
+
|
|
9
|
+
设置 → **插件** → **Happy 远程**:
|
|
10
|
+
|
|
11
|
+
- 未配对:二维码 + 可复制的手机/网页链接。请在 **Happy App 里扫**,不要用系统相机。
|
|
12
|
+
- 已配对:显示已连接,可断开或点 **重新配对** 再出一张新二维码。断开只关掉 Happy 映射,网页 UI 还在。
|
|
13
|
+
- **手机能管多深**:只看 / 能聊 / 能批 / 完整。默认「能批」。改到 `danger-full-access`、以及让手机换模型和思考强度同步到电脑输入框,都需要「完整」。电脑上换模型和思考强度不需要这个档。两边都能切,会互相跟着变。网页加了新模型后,已经同步到手机的旧对话名单也会更新。改档位立刻生效,不必重新配对。
|
|
14
|
+
|
|
15
|
+
手机 Happy App:
|
|
16
|
+
|
|
17
|
+
- 列表里看到本机普通会话(子 agent 不镜像)。侧栏里没点开的对话同样保持在线心跳。网页上的空白「新会话」占位(没发过第一条消息、网页未选中时会藏起来)不会出现在手机上;发出第一条之后才会镜像。在手机里给这样的对话发消息,会把它在电脑上唤醒(网页不必跟着切过去),并用这场对话上次用过的模型生成回复;没有记录就用网页默认模型。连着电脑时是在线;心跳断了会显示未在线。网页侧栏还在的对话会自动把心跳续上;手机点开这条未在线对话也会重新接通,不必先去网页点开。标题优先用网页会话名,没有就用第一条用户消息;空对话和网页一样叫「新会话」。列表按网页工作区分组,不会把 Windows 盘符路径堆成一坨。网页归档的对话会停心跳、进手机归档列表;从归档里打开或再发一条,网页侧栏会把这场对话显示回来。
|
|
18
|
+
- 打开空的 Happy 会话时,会把网页里已有的用户/助手/工具记录补进去。之后网页新消息也会推到手机。从手机新建或唤醒一场对话时,首轮请求会补上明确的思考强度(网页正在用的档,否则模型默认档),并同时认消息里的 `effort` / `effortLevel`,避免第一段思考被写成正文。
|
|
19
|
+
- 新建:只能选已经在网页登记过的工作区。不会在磁盘上 mkdir。App 里「new worktree」会失败(故意不暴露 bash)。
|
|
20
|
+
- 发文字:已注册 `/命令` 走命令通道;否则当聊天。`/skill名` 仍当普通消息,由 harness 注入。
|
|
21
|
+
- 发图片:相册/拍照/粘贴后,会随下一条文字(也可以只发图)进同一场电脑对话,模型能看见图。`.java` / `.txt` 等普通文件会先下载到工作区 `happy-inbox/`,再走电脑原来的 `read` 工具读内容(和你在仓库里打开一个文件一样)。PDF、Word 这类不是纯文本的文件,电脑的 `read` 会拒绝,插件也不会自己拆文档。远程档至少「能聊」。输入框左边应有图片按钮;发出后电脑网页会出现图片,手机会转圈等回复。电脑网页贴的图也会按同一套加密上传推到手机。
|
|
22
|
+
- 工具批准、计划审阅、选择题:和网页黄框/提问框赛跑,谁先点谁算。手机点「始终允许」只在这次网页进程里记住。提问还在等的时候,用输入框打字等于自己填;审阅计划时打字会关掉审阅,这句话当普通聊天发出去。远程档至少「能批」才能从手机点。
|
|
23
|
+
- 电脑正在生成时,手机可以点停止(Happy App 的 Stop)。远程档至少「能聊」。只看档点了不会停。
|
|
24
|
+
- 助手回复按整段推到手机(生成中会转圈)。思考收成一行 **Think · 摘要** 的 Note 卡片,点进去看全文。工具用 Happy 认识的名字(Grep / Read / Bash),描述跟网页那一行摘要一样。Happy 没有网页同款图标。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
发布后:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
dsh plugin --profile web add @sjhmars/happy-bridge
|
|
32
|
+
dsh plugin --profile desktop add @sjhmars/happy-bridge
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
设置里的二维码卡片是浏览器半边:必须按包名装进 profile,Harness 才能扫到 `dsh.client` 和 `exports["./client"]`。只 `--patch` 一个 `file:///` 文件只能挂上 Host,页面上不会出现入口。
|
|
36
|
+
|
|
37
|
+
## 本地联调
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
cd H:\dsh-plugin
|
|
41
|
+
pnpm --filter @sjhmars/happy-bridge run build
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
在 Harness 仓库把本插件目录装进 web profile(file: 依赖,不是把整个插件仓库 pnpm link 进去),然后**不要**再带 `--patch`:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
cd H:\deepseek-harness
|
|
48
|
+
pnpm dsh plugin --profile web add H:\dsh-plugin\plugins\happy-bridge
|
|
49
|
+
pnpm dsh web --port 3080
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
打开设置 → 插件 → 插件配置,应出现 **Happy 远程** 卡片。在 Happy App 里扫码。扫码后手机应能看到本机会话、发消息、收回复。关掉插件或点断开,网页对话不受影响。
|
|
53
|
+
|
|
54
|
+
卸掉本地安装:`pnpm dsh plugin --profile web remove @sjhmars/happy-bridge`。
|
|
55
|
+
|
|
56
|
+
凭据优先读 `~/.happy/access.key`(如果以前用过 Happy CLI);否则写到 `%USERPROFILE%\.dsh\happy-bridge\`。
|
|
57
|
+
|
|
58
|
+
## 配置
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
- insert:
|
|
62
|
+
- id: happy-bridge
|
|
63
|
+
name: '@sjhmars/happy-bridge'
|
|
64
|
+
config:
|
|
65
|
+
enabled: true
|
|
66
|
+
serverUrl: https://api.cluster-fluster.com
|
|
67
|
+
appUrl: https://app.happy.engineering
|
|
68
|
+
pairOnStart: true
|
|
69
|
+
remoteGrant: approve
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`remoteGrant`:`watch` | `chat` | `approve` | `full`。设置页下拉框会写入这份配置。会话里也可以 `/remote approve`(只改当前进程,不写设置)。档位「完整」时,手机换模型和思考强度走电脑同一条 `session.selectModel`,网页模型栏和推理等级会跟着变。电脑自己换模型和思考强度同样会写回手机。网页加了新模型后,已镜像的手机会话会重新收到完整名单。
|
|
73
|
+
|
|
74
|
+
## 已知限制
|
|
75
|
+
|
|
76
|
+
- 第 1 期 `listDirectory` 只列出已登记工作区根,不能在手机里浏览仓库内部文件。
|
|
77
|
+
- 不注册 `bash` / `writeFile` / `ripgrep`,App「new git worktree」不可用。
|
|
78
|
+
- 助手正文仍按整段推送:Happy 的每条 `text` 都是独立气泡,逐 token 推会变成一行一字。
|
|
79
|
+
- 电脑回合失败时,手机会收到一条「电脑没生成回复」说明,而不是空着。
|
|
80
|
+
- Happy 官方会藏掉 `thinking: true` 正文和名为 `think` 的工具,所以思考做成可点开的 Note 卡片,不是原生折叠思考。
|
|
81
|
+
- 手机 App 列表往往只有「归档」没有「删除」。空白「新会话」点归档会从列表拿掉。已经聊过的对话点归档:手机进归档列表,网页侧栏也藏起来。在手机归档列表里打开或再发一条,会重新接通,网页侧栏也会把这场对话显示回来。网页自己点归档同样会停手机心跳;手机再恢复时网页侧栏也会回来。
|
|
82
|
+
- 本插件连的是 Happy 云中继,服务器看不到明文,但需要网络。
|
|
83
|
+
- 旧版镜像出来的空会话:重启网页后插件会删掉这些空白占位,不会再出现。如果手机上仍留着一条空的「新会话」,左滑删掉即可,这次不会再长回来。
|
|
84
|
+
- 手机上多出一条名叫仓库文件夹(例如 `deepseek-harness`)的对话、列表也不再按项目分组:那是旧版把空会话写成了文件夹名、又把 Windows 路径交给 Happy 分组。重启网页后会按工作区重新分组;那条假对话不会再生成,左滑删掉即可。
|
|
85
|
+
- 手机列表全部已断开 / 显示「非活动」:先看设置卡片是否出现「已接通对话 x/y」。x 为 0 时通道还没连上,等一会儿或重启网页。不要去扫配对二维码(那是连账号,不是唤醒旧对话)。手机请在 Happy App 里扫,不要用系统相机。
|
|
86
|
+
- 向 Happy 上报的 CLI 版本与官网 npm 包 `happy` 对齐(当前 1.2.0),避免 App 把插件当成旧 CLI 催更新。仍然不要 `npm install -g happy`。
|
|
87
|
+
- 手机上左滑归档空白占位不会再长回来。已经聊过的对话误点归档后,到归档列表打开或再发一条即可重新接通,网页侧栏也会回来。
|
|
88
|
+
- 手机列表里某场对话变成未在线 / 非活动,而网页侧栏还在:心跳断了。插件会把心跳续上;也可以在手机里点开这条对话重新接通。网页会话不会动。
|
|
89
|
+
- 从手机发出的文字不会再在聊天里回显一遍。
|
|
90
|
+
- 手机图片按 Happy 官方加密上传下载;电脑网页贴的图用同一套 blob 密钥加密后再 `request-upload` 推到手机。不是图片的附件只存到工作区 `happy-inbox/`,由电脑原来的 `read` 工具读取,不会当识图输入,也不会在插件里拆 PDF/Word。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# The @sjhmars/happy-bridge bundle layer: inserts the plugin row that the
|
|
2
|
+
# profile composes. Applied when a profile lists this package in its
|
|
3
|
+
# `dsh.profile.bundles` (which `dsh plugin add` maintains automatically).
|
|
4
|
+
- insert:
|
|
5
|
+
- id: happy-bridge
|
|
6
|
+
name: '@sjhmars/happy-bridge'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@sjhmars/happy-bridge",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region \0dsh-css:H:\dsh-plugin\plugins\happy-bridge\src\client\HappyBridgeCard.module.css.mjs
|
|
10
|
+
const css = ".l9rBEq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}.l9rBEq_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;gap:12px;padding:14px 16px;display:flex}.l9rBEq_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.l9rBEq_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600}.l9rBEq_description{color:var(--dsw-alias-label-tertiary);font-size:13px}.l9rBEq_chevron{color:var(--dsw-alias-label-tertiary)}.l9rBEq_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:12px;margin:0 16px;padding:12px 0 16px;display:flex}.l9rBEq_status{color:var(--dsw-alias-label-secondary);font-size:13px}.l9rBEq_error{color:var(--dsw-alias-danger,#c44);font-size:13px}.l9rBEq_qr{background:#fff;border-radius:8px;align-self:center;width:180px;height:180px}.l9rBEq_row{flex-wrap:wrap;gap:8px;display:flex}.l9rBEq_button{appearance:none;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:8px;padding:6px 12px;font-size:13px}.l9rBEq_button:hover{background:var(--dsw-alias-interactive-bg-hover)}.l9rBEq_label{color:var(--dsw-alias-label-tertiary);font-size:13px}.l9rBEq_select,.l9rBEq_checkbox{font:inherit;font-size:13px}.l9rBEq_select{min-width:8em}.l9rBEq_select:disabled,.l9rBEq_checkbox:disabled{opacity:.6;cursor:not-allowed}";
|
|
11
|
+
const tagId = "@sjhmars/happy-bridge/HappyBridgeCard.module.css";
|
|
12
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
13
|
+
const tag = document.createElement("style");
|
|
14
|
+
tag.dataset.plugin = "@sjhmars/happy-bridge";
|
|
15
|
+
tag.dataset.pluginCss = tagId;
|
|
16
|
+
tag.textContent = css;
|
|
17
|
+
document.head.appendChild(tag);
|
|
18
|
+
}
|
|
19
|
+
var HappyBridgeCard_module_css_default = {
|
|
20
|
+
"error": "l9rBEq_error",
|
|
21
|
+
"name": "l9rBEq_name",
|
|
22
|
+
"qr": "l9rBEq_qr",
|
|
23
|
+
"header": "l9rBEq_header",
|
|
24
|
+
"body": "l9rBEq_body",
|
|
25
|
+
"headText": "l9rBEq_headText",
|
|
26
|
+
"status": "l9rBEq_status",
|
|
27
|
+
"card": "l9rBEq_card",
|
|
28
|
+
"description": "l9rBEq_description",
|
|
29
|
+
"label": "l9rBEq_label",
|
|
30
|
+
"select": "l9rBEq_select",
|
|
31
|
+
"checkbox": "l9rBEq_checkbox",
|
|
32
|
+
"row": "l9rBEq_row",
|
|
33
|
+
"chevron": "l9rBEq_chevron",
|
|
34
|
+
"button": "l9rBEq_button"
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/client/HappyBridgeCard.tsx
|
|
38
|
+
/** Settings → Plugins card: pairing QR, copy links, remote grant. */
|
|
39
|
+
const GRANTS = [
|
|
40
|
+
"watch",
|
|
41
|
+
"chat",
|
|
42
|
+
"approve",
|
|
43
|
+
"full"
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Render the Happy remote plugin card.
|
|
47
|
+
* @param props - locale + injected Host RPC.
|
|
48
|
+
*/
|
|
49
|
+
function HappyBridgeCard(props) {
|
|
50
|
+
const { t, getStatus, startPairing, disconnect, rePair, setGrant, setEnabled } = props;
|
|
51
|
+
const snap = props.useHappySettings((snapshot) => snapshot);
|
|
52
|
+
const grant = snap.value?.remoteGrant ?? "approve";
|
|
53
|
+
const enabled = snap.value?.enabled !== false;
|
|
54
|
+
const writable = snap.writable;
|
|
55
|
+
const [open, setOpen] = (0, react.useState)(true);
|
|
56
|
+
const [status, setStatus] = (0, react.useState)(void 0);
|
|
57
|
+
const [copied, setCopied] = (0, react.useState)(void 0);
|
|
58
|
+
(0, react.useEffect)(() => {
|
|
59
|
+
let cancelled = false;
|
|
60
|
+
const tick = () => {
|
|
61
|
+
getStatus().then((next) => {
|
|
62
|
+
if (!cancelled) setStatus(next);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
tick();
|
|
66
|
+
const timer = setInterval(tick, 2e3);
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
clearInterval(timer);
|
|
70
|
+
};
|
|
71
|
+
}, [getStatus]);
|
|
72
|
+
const copy = async (which, value) => {
|
|
73
|
+
if (value === void 0) return;
|
|
74
|
+
await navigator.clipboard.writeText(value);
|
|
75
|
+
setCopied(which);
|
|
76
|
+
setTimeout(() => setCopied(void 0), 1500);
|
|
77
|
+
};
|
|
78
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
79
|
+
className: HappyBridgeCard_module_css_default.card,
|
|
80
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
81
|
+
type: "button",
|
|
82
|
+
className: HappyBridgeCard_module_css_default.header,
|
|
83
|
+
"aria-expanded": open,
|
|
84
|
+
onClick: () => setOpen(!open),
|
|
85
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
86
|
+
className: HappyBridgeCard_module_css_default.headText,
|
|
87
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
88
|
+
className: HappyBridgeCard_module_css_default.name,
|
|
89
|
+
children: t("title")
|
|
90
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
91
|
+
className: HappyBridgeCard_module_css_default.description,
|
|
92
|
+
children: t("description")
|
|
93
|
+
})]
|
|
94
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
95
|
+
className: HappyBridgeCard_module_css_default.chevron,
|
|
96
|
+
children: open ? "▴" : "▾"
|
|
97
|
+
})]
|
|
98
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
99
|
+
className: HappyBridgeCard_module_css_default.body,
|
|
100
|
+
children: [
|
|
101
|
+
!writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
102
|
+
className: HappyBridgeCard_module_css_default.status,
|
|
103
|
+
role: "status",
|
|
104
|
+
children: t("readOnly")
|
|
105
|
+
}) : null,
|
|
106
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
107
|
+
className: HappyBridgeCard_module_css_default.label,
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
110
|
+
className: HappyBridgeCard_module_css_default.checkbox,
|
|
111
|
+
type: "checkbox",
|
|
112
|
+
checked: enabled,
|
|
113
|
+
disabled: !writable,
|
|
114
|
+
onChange: (event) => void setEnabled(event.target.checked)
|
|
115
|
+
}),
|
|
116
|
+
" ",
|
|
117
|
+
t("enabled")
|
|
118
|
+
]
|
|
119
|
+
}),
|
|
120
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
121
|
+
className: HappyBridgeCard_module_css_default.status,
|
|
122
|
+
children: status?.paired === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [t("status.paired"), typeof status.sessionCount === "number" ? ` · ${t("status.linked")} ${status.linkedCount ?? 0}/${status.sessionCount}` : null] }) : status?.pairing === true ? t("status.pairing") : t("status.unpaired")
|
|
123
|
+
}),
|
|
124
|
+
status?.error !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
125
|
+
className: HappyBridgeCard_module_css_default.error,
|
|
126
|
+
role: "status",
|
|
127
|
+
children: [
|
|
128
|
+
t("error"),
|
|
129
|
+
": ",
|
|
130
|
+
status.error
|
|
131
|
+
]
|
|
132
|
+
}) : null,
|
|
133
|
+
status?.qrDataUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
134
|
+
className: HappyBridgeCard_module_css_default.qr,
|
|
135
|
+
alt: t("qr.alt"),
|
|
136
|
+
src: status.qrDataUrl
|
|
137
|
+
}) : null,
|
|
138
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
139
|
+
className: HappyBridgeCard_module_css_default.row,
|
|
140
|
+
children: [
|
|
141
|
+
status?.paired === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
142
|
+
type: "button",
|
|
143
|
+
className: HappyBridgeCard_module_css_default.button,
|
|
144
|
+
onClick: () => void disconnect(),
|
|
145
|
+
children: t("disconnect")
|
|
146
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
147
|
+
type: "button",
|
|
148
|
+
className: HappyBridgeCard_module_css_default.button,
|
|
149
|
+
onClick: () => void startPairing(),
|
|
150
|
+
children: t("start")
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
153
|
+
type: "button",
|
|
154
|
+
className: HappyBridgeCard_module_css_default.button,
|
|
155
|
+
onClick: () => void rePair(),
|
|
156
|
+
children: t("repair")
|
|
157
|
+
}),
|
|
158
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
159
|
+
type: "button",
|
|
160
|
+
className: HappyBridgeCard_module_css_default.button,
|
|
161
|
+
onClick: () => void copy("mobile", status?.mobileUrl),
|
|
162
|
+
children: copied === "mobile" ? t("copied") : t("copy.mobile")
|
|
163
|
+
}),
|
|
164
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
165
|
+
type: "button",
|
|
166
|
+
className: HappyBridgeCard_module_css_default.button,
|
|
167
|
+
onClick: () => void copy("web", status?.webUrl),
|
|
168
|
+
children: copied === "web" ? t("copied") : t("copy.web")
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
}),
|
|
172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
173
|
+
className: HappyBridgeCard_module_css_default.label,
|
|
174
|
+
children: [
|
|
175
|
+
t("grant"),
|
|
176
|
+
" ",
|
|
177
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
178
|
+
className: HappyBridgeCard_module_css_default.select,
|
|
179
|
+
value: grant,
|
|
180
|
+
disabled: !writable,
|
|
181
|
+
onChange: (event) => void setGrant(event.target.value),
|
|
182
|
+
children: GRANTS.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
183
|
+
value,
|
|
184
|
+
children: t(`grant.${value}`)
|
|
185
|
+
}, value))
|
|
186
|
+
})
|
|
187
|
+
]
|
|
188
|
+
}),
|
|
189
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
190
|
+
className: HappyBridgeCard_module_css_default.status,
|
|
191
|
+
children: t("grant.hint")
|
|
192
|
+
})
|
|
193
|
+
]
|
|
194
|
+
}) : null]
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/client/locales.ts
|
|
199
|
+
/** Locale namespace owned by the Happy remote settings card. */
|
|
200
|
+
const NS = "happy-bridge";
|
|
201
|
+
/** Simplified-Chinese copy. */
|
|
202
|
+
const zh = {
|
|
203
|
+
"title": "Happy 远程",
|
|
204
|
+
"description": "用手机 Happy App 遥控已经在跑的对话。",
|
|
205
|
+
"expand": "展开",
|
|
206
|
+
"collapse": "收起",
|
|
207
|
+
"status.unpaired": "还没配对",
|
|
208
|
+
"status.pairing": "等待手机扫码…请在 Happy App 里扫,不要用系统相机。",
|
|
209
|
+
"status.paired": "已连接到 Happy",
|
|
210
|
+
"status.linked": "已接通对话",
|
|
211
|
+
"qr.alt": "Happy 配对二维码",
|
|
212
|
+
"copy.mobile": "复制手机链接",
|
|
213
|
+
"copy.web": "复制网页链接",
|
|
214
|
+
"copied": "已复制",
|
|
215
|
+
"start": "开始配对",
|
|
216
|
+
"repair": "重新配对",
|
|
217
|
+
"disconnect": "断开",
|
|
218
|
+
"grant": "手机能管多深",
|
|
219
|
+
"grant.watch": "只看",
|
|
220
|
+
"grant.chat": "能聊",
|
|
221
|
+
"grant.approve": "能批",
|
|
222
|
+
"grant.full": "完整",
|
|
223
|
+
"grant.hint": "改成「完整」后,手机换模型和思考强度会同步到电脑。电脑上换也会同步到手机。",
|
|
224
|
+
"readOnly": "这份设置现在不能改。",
|
|
225
|
+
"enabled": "启用",
|
|
226
|
+
"error": "出错"
|
|
227
|
+
};
|
|
228
|
+
/** English copy. */
|
|
229
|
+
const en = {
|
|
230
|
+
"title": "Happy remote",
|
|
231
|
+
"description": "Let Happy App on your phone drive the same running conversations.",
|
|
232
|
+
"expand": "Expand",
|
|
233
|
+
"collapse": "Collapse",
|
|
234
|
+
"status.unpaired": "Not paired",
|
|
235
|
+
"status.pairing": "Waiting for a scan inside Happy App — not the OS camera.",
|
|
236
|
+
"status.paired": "Connected to Happy",
|
|
237
|
+
"status.linked": "Linked sessions",
|
|
238
|
+
"qr.alt": "Happy pairing QR code",
|
|
239
|
+
"copy.mobile": "Copy phone link",
|
|
240
|
+
"copy.web": "Copy web link",
|
|
241
|
+
"copied": "Copied",
|
|
242
|
+
"start": "Start pairing",
|
|
243
|
+
"repair": "Pair again",
|
|
244
|
+
"disconnect": "Disconnect",
|
|
245
|
+
"grant": "How far the phone may control",
|
|
246
|
+
"grant.watch": "Watch",
|
|
247
|
+
"grant.chat": "Chat",
|
|
248
|
+
"grant.approve": "Approve",
|
|
249
|
+
"grant.full": "Full",
|
|
250
|
+
"grant.hint": "On Full, a phone model or thinking-effort switch updates the computer picker. A computer switch updates the phone too.",
|
|
251
|
+
"readOnly": "This document is not writable here.",
|
|
252
|
+
"enabled": "Enabled",
|
|
253
|
+
"error": "Error"
|
|
254
|
+
};
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/client/index.ts
|
|
257
|
+
/** Required client services. */
|
|
258
|
+
const inject = [
|
|
259
|
+
"slots",
|
|
260
|
+
"locale",
|
|
261
|
+
"connection",
|
|
262
|
+
"remote",
|
|
263
|
+
"settingsScope"
|
|
264
|
+
];
|
|
265
|
+
/**
|
|
266
|
+
* Register the Happy remote card under the `happy-bridge` settings namespace.
|
|
267
|
+
* @param ctx - browser plugin context.
|
|
268
|
+
*/
|
|
269
|
+
function apply(ctx) {
|
|
270
|
+
const connection = ctx.get("connection");
|
|
271
|
+
const scope = ctx.settingsScope.bind({ namespace: "happy-bridge" });
|
|
272
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
273
|
+
zh,
|
|
274
|
+
en
|
|
275
|
+
}), "happy-bridge: browser dictionaries");
|
|
276
|
+
const rpc = async (method) => {
|
|
277
|
+
return unwrap(await connection.rpc.call("/api", `happyBridge/${method}`, { args: {} }));
|
|
278
|
+
};
|
|
279
|
+
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
280
|
+
name: "settings.plugin.item",
|
|
281
|
+
key: "happy-bridge",
|
|
282
|
+
locale: NS,
|
|
283
|
+
inject: () => ({
|
|
284
|
+
getStatus: () => rpc("getStatus"),
|
|
285
|
+
startPairing: () => rpc("startPairing"),
|
|
286
|
+
disconnect: () => rpc("disconnect"),
|
|
287
|
+
rePair: () => rpc("rePair"),
|
|
288
|
+
setGrant: (grant) => scope.set("remoteGrant", grant),
|
|
289
|
+
setEnabled: (enabled) => scope.set("enabled", enabled),
|
|
290
|
+
hooks: { happySettings: scope }
|
|
291
|
+
})
|
|
292
|
+
}, HappyBridgeCard));
|
|
293
|
+
}
|
|
294
|
+
function unwrap(result) {
|
|
295
|
+
if (result.ok) return result.value;
|
|
296
|
+
throw new Error(result.error.message);
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
exports.apply = apply;
|
|
300
|
+
exports.inject = inject;
|
|
301
|
+
return module.exports;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
//# sourceMappingURL=client.js.map
|