chore: 初始化 dsh-enhancements 留档仓库
- 插件源码:dsh-usage(用户头像 + DeepSeek 官方 API 消耗面板) - 工具:dl-gh.mjs(node fetch 下载 gh,绕开沙箱 curl 限制) - 文档:PLAN.md 路线图 + mmx 视觉桥接补丁说明 - 资产:DeepSeek 鲸鱼图标
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# 依赖与本地缓存
|
||||||
|
node_modules/
|
||||||
|
tools/gh-download/
|
||||||
|
tools/gh-extract/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 凭据与敏感信息(绝不入库)
|
||||||
|
.credentials.yaml
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
.env
|
||||||
|
|
||||||
|
# 系统文件
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# DSH 本地状态
|
||||||
|
sessions/
|
||||||
|
storages/
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# dsh-enhancements — DeepSeek Harness 增强插件集
|
||||||
|
|
||||||
|
把 DeepSeek Harness 升级为 **Codex 级智能助手** 的全部开发成果归档于此:
|
||||||
|
插件源码、桌面应用壳脚本、视觉桥接补丁、文档与规划。
|
||||||
|
|
||||||
|
> 由本机 agent 会话持续开发,随开发进度同步推送到 GitHub / Gitea。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
dsh-enhancements/
|
||||||
|
├── README.md # 本文件
|
||||||
|
├── docs/
|
||||||
|
│ ├── PLAN.md # 总体规划(Codex 化路线图)
|
||||||
|
│ └── mmx-image-bridge-PATCH.md # 聊天发图→mmx 视觉识别 桥接补丁说明
|
||||||
|
├── plugins/ # 每个插件一个目录(源码 + 说明)
|
||||||
|
│ └── dsh-usage/ # 用户头像 + API 消耗面板
|
||||||
|
├── presets/ # Agent 预设(落盘形式,重启可用)
|
||||||
|
├── tools/ # 辅助脚本
|
||||||
|
│ ├── dl-gh.mjs # 用 node fetch 下载 gh CLI(沙箱内 curl 被禁时的替代)
|
||||||
|
│ └── gh-download/ # 下载缓存(不入库,见 .gitignore)
|
||||||
|
└── assets/ # 图标等资源
|
||||||
|
└── dsh.ico # DeepSeek 鲸鱼图标(托盘/快捷方式用)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 已包含
|
||||||
|
|
||||||
|
- **桌面应用壳**:`C:\Users\a1703\.dsh\bin\dsh-app.ps1`(托盘 + Edge 独立窗口)与桌面快捷方式 `DeepSeek Harness.lnk` —— 见 docs/ 说明
|
||||||
|
- **视觉桥接**:聊天窗口发图 → MiniMax M3(NVIDIA 托管)识别
|
||||||
|
- **dsh-usage 插件**:侧边栏底部 🐋 用户头像 → DeepSeek 官方余额 API 消耗面板
|
||||||
|
|
||||||
|
## 安全说明
|
||||||
|
|
||||||
|
- 本仓库**绝不包含**任何 API Key、Token、密码。
|
||||||
|
- 部署机上的凭据位于 `%DSH_HOME%\.credentials.yaml`,由运行时读取,不入库。
|
||||||
|
- Gitea 等远程地址的凭据使用系统凭据管理器,不写入 `.git/config`。
|
||||||
|
|
||||||
|
## 快速开始(新机器)
|
||||||
|
|
||||||
|
1. 安装 Node.js ≥ 18(带全局 fetch)
|
||||||
|
2. 把 `tools/dl-gh.mjs` 下载的 gh 解压到 PATH,或直接用 git
|
||||||
|
3. 按 `docs/PLAN.md` 各阶段说明启用插件
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,66 @@
|
|||||||
|
# 总体规划:把 DeepSeek Harness 升级为 Codex 级智能助手
|
||||||
|
|
||||||
|
> 状态:进行中。本文件是路线图,随开发更新。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
用户头像/API 消耗、GitHub/Gitea 集成、定时任务、电脑操作模式(截图+视觉+鼠标键盘)、
|
||||||
|
内置浏览器操控、吉祥物悬浮助手,全部成果留档并发布到 GitHub/Gitea 仓库。
|
||||||
|
|
||||||
|
## 现状盘点(已验证)
|
||||||
|
|
||||||
|
- 桌面应用化:快捷方式 → 托盘(dsh-app.ps1)→ Edge 独立窗口 ✅
|
||||||
|
- 聊天发图 + 视觉理解:mmx 桥接(MiniMax M3 via NVIDIA)✅
|
||||||
|
- 动态 Cordis 插件(Host/Client 双向)✅
|
||||||
|
- git 2.55 + GitHub(jiangchen1245)+ 自建 Gitea(gitea.truestudio.online)✅
|
||||||
|
- DeepSeek 官方余额 API 可用(GET https://api.deepseek.com/user/balance)✅
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
### 1️⃣ GitHub / Gitea 集成(进行中)
|
||||||
|
- [x] 验证 Gitea 凭据(git 协议 + API basic auth)
|
||||||
|
- [ ] 装 gh CLI(沙箱内 github CDN 不通,需换网络或用户手动装)
|
||||||
|
- [ ] dsh-git 插件:repo_status / gh_issue / gh_pr / gh_commit / gh_push
|
||||||
|
- [ ] GUI"Git 中心"面板
|
||||||
|
|
||||||
|
### 2️⃣ 定时任务插件
|
||||||
|
- [ ] dsh-scheduler:cron 表达式 + 动作(命令/工具调用)
|
||||||
|
- [ ] 持久化(重启不丢)+ GUI 管理面板
|
||||||
|
|
||||||
|
### 3️⃣ 电脑操作模式(核心)
|
||||||
|
- [ ] screen_capture(截图)
|
||||||
|
- [ ] screen_vision(mmx M3 视觉识别元素/坐标)
|
||||||
|
- [ ] screen_click / screen_type / screen_key(user32)
|
||||||
|
- [ ] screen_list_windows / screen_ui_tree(UI Automation)
|
||||||
|
- 闭环:截图 → 看懂 → 操作 → 再看
|
||||||
|
|
||||||
|
### 4️⃣ 内置浏览器操控
|
||||||
|
- [ ] Edge CDP 驱动(导航/点击/填表/截页)
|
||||||
|
- [ ] GUI 内嵌浏览器面板(webServer 反代 iframe)
|
||||||
|
|
||||||
|
### 5️⃣ 吉祥物悬浮助手
|
||||||
|
- [ ] shell.overlay 右下角 🐋 浮窗 + 迷你对话 + 快捷按钮
|
||||||
|
|
||||||
|
### 6️⃣ 体验打磨
|
||||||
|
- [ ] Agent Presets(computer-use / dev)
|
||||||
|
- [ ] 快捷键、布局优化、开机自启
|
||||||
|
|
||||||
|
### 7️⃣ 留档与发布
|
||||||
|
- [x] 本地仓库 D:\project\dsh-enhancements(git init + 结构)
|
||||||
|
- [ ] 推送到 Gitea jiangchen/dsh-enhancements
|
||||||
|
- [ ] 推送到 GitHub jiangchen1245/dsh-enhancements
|
||||||
|
- [ ] 脱敏检查(无 Key/Token/密码)
|
||||||
|
|
||||||
|
## 技术要点
|
||||||
|
|
||||||
|
- 沙箱:curl/winget 的 HTTPS 被禁,node fetch 可用 → 网络操作统一走 node
|
||||||
|
- 客户端无 window/document/fetch → 打开外链由 Host 侧 shell 完成
|
||||||
|
- 动态插件不持久 → 每个插件同时落盘为 presets/ 文件与源码归档
|
||||||
|
- 升级覆盖 → mmx 桥接补丁保留重放脚本(PATCH.md)
|
||||||
|
- 凭据 → 一律系统凭据管理器,不入库、不进 .git/config
|
||||||
|
|
||||||
|
## 安全红线
|
||||||
|
|
||||||
|
- 仓库绝不包含 API Key / Token / 密码
|
||||||
|
- Gitea remote 不用内嵌密码 URL;用凭据管理器
|
||||||
|
- .credentials.yaml 只留在部署机
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# MMX 图片桥接补丁(聊天窗口发图 → mmx-vision 技能识别)
|
||||||
|
|
||||||
|
日期:2026-08-12
|
||||||
|
目的:让 DeepSeek Harness 的聊天窗口可以直接发送/粘贴图片。图片被持久化为附件,
|
||||||
|
发给模型时由适配器转成"附件路径文本提示",agent 收到后用 mmx-vision 技能(本地
|
||||||
|
Node 脚本调用 NVIDIA 托管的 MiniMax M3)识别或按用户意图取用。
|
||||||
|
|
||||||
|
## 相关自定义(本机桌面应用壳)
|
||||||
|
|
||||||
|
- `C:\Users\a1703\.dsh\bin\dsh-app.ps1`:托盘应用(无窗口后台运行 dsh web + 系统托盘,
|
||||||
|
右键"打开主界面 / 退出",打开用 Edge `--app` 独立窗口)。快捷方式
|
||||||
|
`桌面\DeepSeek Harness.lnk` 指向它(powershell -WindowStyle Hidden -File)。
|
||||||
|
- `C:\Users\a1703\.dsh\assets\dsh.ico`:多尺寸(16-256)DeepSeek 鲸鱼图标,
|
||||||
|
由 `favicon.svg` 经 Edge 无头渲染 + System.Drawing 缩放生成,供托盘/快捷方式使用。
|
||||||
|
- 旧版 `C:\Users\a1703\.dsh\bin\dsh-web.cmd`:手动模式(显示控制台窗口),备用。
|
||||||
|
|
||||||
|
## 修改的文件(全部位于 npx 缓存安装,重启 Harness 生效)
|
||||||
|
|
||||||
|
### 0. @deepseek-ai/dsh-web-frontend/dist(应用窗口图标)
|
||||||
|
- 新增 `favicon-256.png`、`favicon.ico`(多尺寸鲸鱼图标)。
|
||||||
|
- `index.html`:favicon 链接增加 `favicon-256.png`(放在 svg 前)。
|
||||||
|
- `manifest.webmanifest`:icons 增加 png/ico 条目。
|
||||||
|
- 作用:Edge `--app` 独立窗口与浏览器标签显示 DeepSeek 鲸鱼图标而不是 Edge 图标。
|
||||||
|
静态文件实时生效(服务直接读磁盘),无需重启。
|
||||||
|
|
||||||
|
### 1. @deepseek-ai/dsh-llm-deepseek/lib/index.js(deepseek 适配器)
|
||||||
|
- 原行为:内容含 image 块时抛 `UNSUPPORTED_CONTENT`(拒绝整轮对话)。
|
||||||
|
- 新行为:`assertTextOnly` 改为 `imageBlocksText`,把每个 image 块序列化成一行
|
||||||
|
`[用户发送的图片附件 id=sha256:xxx 类型=image/png 文件=<DSH_HOME>/attachments/v1/objects/xx/xxx]`
|
||||||
|
文本;`serializeMessages` 的用户消息文本 = 原文本 + 图片提示。
|
||||||
|
- 附件路径规则与 `@deepseek-ai/dsh-attachment-local` 的存储约定一致
|
||||||
|
(对象路径 `objects/<sha前2位>/<sha256>`,attachmentId 前缀 `sha256:`)。
|
||||||
|
|
||||||
|
### 2. @deepseek-ai/dsh-host-apiproxy/lib/index.js(host API 代理)
|
||||||
|
- `prompt` RPC:删除"模型 inputModalities 不含 image 就拒绝(MODEL_DOES_NOT_SUPPORT_IMAGES)"检查,
|
||||||
|
图片一律放行(由适配器文本化)。
|
||||||
|
- `selectModel` RPC:删除"会话已含图片时禁止切换到不支持图片的模型"检查。
|
||||||
|
|
||||||
|
## 生效条件
|
||||||
|
- 重启 Harness 进程后生效(node_modules 中的运行时文件改动不会热加载)。
|
||||||
|
- 备份:同目录 `index.js.mmx-bak`(每个改过的包旁边)。
|
||||||
|
|
||||||
|
## 升级后重放
|
||||||
|
重新安装/升级 @deepseek-ai/dsh 后,以上两处改动会被覆盖。重放步骤:
|
||||||
|
1. 用 `node --check` 验证改动文件语法。
|
||||||
|
2. 对每个包:把 `assertTextOnly` 的抛错逻辑替换为 `imageBlocksText`(见下),
|
||||||
|
并删除 api-proxy 两处图片能力检查。
|
||||||
|
3. 或直接 diff 当前 `.mmx-bak` 与新文件,把差异搬过去。
|
||||||
|
|
||||||
|
## 不影响
|
||||||
|
- 客户端 UI 无需改动(图片 rail/拖放/粘贴本来就完整)。
|
||||||
|
- pi-ai 模型路由(真多模态)不受影响。
|
||||||
|
- 会话历史中的图片块在每次模型调用时都会文本化,客户端仍按附件渲染缩略图。
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# dsh-usage — 用户头像 + API 消耗面板
|
||||||
|
|
||||||
|
在 DeepSeek Harness 侧边栏底部(设置旁)添加一个 Codex 式**用户头像**入口,
|
||||||
|
点击弹出模态面板,展示 **DeepSeek 官方 API 余额/消耗**,并可一键打开官网用量页。
|
||||||
|
|
||||||
|
## 效果
|
||||||
|
|
||||||
|
- 侧边栏底部(设置按钮旁):圆形 🐋 头像按钮(窄栏只显图标,宽栏带"API 用量"文字)
|
||||||
|
- 点击 → 居中模态面板:
|
||||||
|
- 账户可用状态
|
||||||
|
- 各币种:总额 / 赠送余额 / 充值余额(来自官方 `GET https://api.deepseek.com/user/balance`)
|
||||||
|
- 按钮:刷新 / 打开官网用量页 / 关闭
|
||||||
|
|
||||||
|
## 实现
|
||||||
|
|
||||||
|
| 端 | 机制 |
|
||||||
|
|---|---|
|
||||||
|
| Host | `harness.handle('usage/balance')`:shell 运行 node 脚本读取 `%DSH_HOME%\.credentials.yaml` 的 `DEEPSEEK_API_KEY`,调用官方余额 API,返回 JSON(密钥不出进程日志);`usage/openPortal`:打开官网用量页 |
|
||||||
|
| Client | `sidebar.footer.action` 注册头像按钮;`shell.overlay` 注册模态面板;两个组件通过插件内微型 store 共享开关状态;样式用主题 CSS 变量(`--dsw-alias-*`) |
|
||||||
|
|
||||||
|
## 为什么用官方 API
|
||||||
|
|
||||||
|
DeepSeek 官方提供余额接口(`/user/balance`),返回 `is_available` + `balance_infos[]`。
|
||||||
|
用量明细官网暂无公开 API,故面板提供"打开官网用量页"(platform.deepseek.com/usage)兜底。
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- `host.js` — Host 半(RPC + 官方 API 调用)
|
||||||
|
- `client.js` — Client 半(头像 + 模态 UI)
|
||||||
|
- 部署方式:通过动态 Cordis 插件(`cordis_define` / `cordis_run`)或本仓库 `presets/` 下的预设文件
|
||||||
|
|
||||||
|
## 前置
|
||||||
|
|
||||||
|
- `%DSH_HOME%\.credentials.yaml` 中存在 `DEEPSEEK_API_KEY`
|
||||||
|
- 官方余额 API 需可访问(api.deepseek.com)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// dsh-usage 插件 — Client 半
|
||||||
|
// 注册两处 UI:
|
||||||
|
// sidebar.footer.action 'dsh-usage-avatar' — 设置旁的 🐋 用户头像按钮
|
||||||
|
// shell.overlay 'dsh-usage-modal' — 点击头像弹出的官方余额模态面板
|
||||||
|
// 两个组件通过插件内微型 store(open + listeners)共享开关状态。
|
||||||
|
return {
|
||||||
|
apply(ctx) {
|
||||||
|
const slots = ctx.get('slots')
|
||||||
|
if (slots === undefined) return
|
||||||
|
styles.insert(`
|
||||||
|
.dsh-ua-avatar { display:flex; align-items:center; gap:8px; width:100%; padding:7px 10px; background:transparent; border:0; border-radius:8px; color:var(--dsw-alias-label-primary); cursor:pointer; font:inherit; }
|
||||||
|
.dsh-ua-avatar:hover { background:var(--dsw-alias-bg-layer-2); }
|
||||||
|
.dsh-ua-avatar.active { background:var(--dsw-alias-bg-layer-2); }
|
||||||
|
.dsh-ua-avatar-icon { width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:15px; flex:none; background:var(--dsw-alias-bg-layer-2); border:1px solid var(--dsw-alias-border-l1); }
|
||||||
|
.dsh-ua-avatar-label { font-size:12px; color:var(--dsw-alias-label-secondary); }
|
||||||
|
.dsh-ua-backdrop { position:fixed; inset:0; z-index:9999; background:rgba(0,0,0,.35); display:flex; align-items:center; justify-content:center; pointer-events:auto; }
|
||||||
|
.dsh-ua-modal { width:380px; max-width:92vw; background:var(--dsw-alias-bg-overlay); border:1px solid var(--dsw-alias-border-l1); border-radius:12px; box-shadow:0 12px 40px rgba(0,0,0,.3); padding:16px; color:var(--dsw-alias-label-primary); pointer-events:auto; font-size:13px; }
|
||||||
|
.dsh-ua-modal h3 { margin:0 0 12px; font-size:15px; display:flex; align-items:center; gap:8px; }
|
||||||
|
.dsh-ua-row { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--dsw-alias-border-l1); }
|
||||||
|
.dsh-ua-row:last-child { border-bottom:0; }
|
||||||
|
.dsh-ua-row .k { color:var(--dsw-alias-label-secondary); }
|
||||||
|
.dsh-ua-row .v { font-weight:600; }
|
||||||
|
.dsh-ua-ok { color:var(--dsw-alias-state-success-primary); }
|
||||||
|
.dsh-ua-err { color:var(--dsw-alias-state-error-primary); }
|
||||||
|
.dsh-ua-btns { display:flex; gap:8px; margin-top:14px; }
|
||||||
|
.dsh-ua-btn { flex:1; padding:7px 10px; border-radius:8px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1); color:var(--dsw-alias-label-primary); cursor:pointer; font:inherit; font-size:12px; }
|
||||||
|
.dsh-ua-btn:hover { background:var(--dsw-alias-bg-layer-2); }
|
||||||
|
.dsh-ua-btn.primary { background:var(--dsw-alias-brand-primary); color:#fff; border-color:transparent; }
|
||||||
|
`)
|
||||||
|
const store = { open: false, listeners: new Set() }
|
||||||
|
const setOpen = (v) => { store.open = v; store.listeners.forEach((fn) => fn()) }
|
||||||
|
const useOpen = () => {
|
||||||
|
const [s, setS] = React.useState(store.open)
|
||||||
|
React.useEffect(() => { store.listeners.add(setS); return () => store.listeners.delete(setS) }, [])
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
const UsageModal = (props) => {
|
||||||
|
const [state, setState] = React.useState({ loading: true, data: null, error: null })
|
||||||
|
const load = () => {
|
||||||
|
setState({ loading: true, data: null, error: null })
|
||||||
|
host.call('usage/balance', {}).then((data) => {
|
||||||
|
setState({ loading: false, data: data, error: null })
|
||||||
|
}).catch((e) => {
|
||||||
|
setState({ loading: false, data: null, error: String((e && e.message) || e) })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
React.useEffect(() => { load() }, [])
|
||||||
|
const openPortal = () => { host.call('usage/openPortal', {}) }
|
||||||
|
const data = state.data
|
||||||
|
const rows = []
|
||||||
|
if (data && Array.isArray(data.balance_infos)) {
|
||||||
|
data.balance_infos.forEach((b) => {
|
||||||
|
rows.push(React.createElement('div', { className: 'dsh-ua-row', key: b.currency + '-total' },
|
||||||
|
React.createElement('span', { className: 'k' }, '总额 (' + b.currency + ')'),
|
||||||
|
React.createElement('span', { className: 'v' }, String(b.total_balance))))
|
||||||
|
rows.push(React.createElement('div', { className: 'dsh-ua-row', key: b.currency + '-granted' },
|
||||||
|
React.createElement('span', { className: 'k' }, '赠送余额'),
|
||||||
|
React.createElement('span', { className: 'v' }, String(b.granted_balance))))
|
||||||
|
rows.push(React.createElement('div', { className: 'dsh-ua-row', key: b.currency + '-topped' },
|
||||||
|
React.createElement('span', { className: 'k' }, '充值余额'),
|
||||||
|
React.createElement('span', { className: 'v' }, String(b.topped_up_balance))))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let body = null
|
||||||
|
if (state.loading) {
|
||||||
|
body = React.createElement('div', { className: 'dsh-ua-row' }, React.createElement('span', { className: 'k' }, '正在查询官方余额…'))
|
||||||
|
} else if (state.error || (data && data.error)) {
|
||||||
|
body = React.createElement('div', { className: 'dsh-ua-err' }, String(state.error || (data && data.error)))
|
||||||
|
} else {
|
||||||
|
body = React.createElement('div', null,
|
||||||
|
React.createElement('div', { className: 'dsh-ua-row' },
|
||||||
|
React.createElement('span', { className: 'k' }, '账户可用'),
|
||||||
|
React.createElement('span', { className: 'v ' + (data.is_available ? 'dsh-ua-ok' : 'dsh-ua-err') }, data.is_available ? '可用' : '不可用')),
|
||||||
|
rows)
|
||||||
|
}
|
||||||
|
return React.createElement('div', { className: 'dsh-ua-backdrop', onClick: props.onClose },
|
||||||
|
React.createElement('div', { className: 'dsh-ua-modal', onClick: (e) => e.stopPropagation() },
|
||||||
|
React.createElement('h3', null, '🐋 DeepSeek API 用量',
|
||||||
|
React.createElement('span', { style: { marginLeft: 'auto', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, '官方余额')),
|
||||||
|
body,
|
||||||
|
React.createElement('div', { className: 'dsh-ua-btns' },
|
||||||
|
React.createElement('button', { className: 'dsh-ua-btn', onClick: load }, '刷新'),
|
||||||
|
React.createElement('button', { className: 'dsh-ua-btn', onClick: openPortal }, '打开官网用量页'),
|
||||||
|
React.createElement('button', { className: 'dsh-ua-btn primary', onClick: props.onClose }, '关闭'))))
|
||||||
|
}
|
||||||
|
|
||||||
|
slots.inject('sidebar.footer.action', () => slots.register(
|
||||||
|
{ name: 'sidebar.footer.action', id: 'dsh-usage-avatar', order: 100, label: 'API 用量' },
|
||||||
|
(props) => {
|
||||||
|
const open = useOpen()
|
||||||
|
return React.createElement('button', {
|
||||||
|
className: 'dsh-ua-avatar' + (open ? ' active' : ''),
|
||||||
|
title: 'API 用量 / 余额',
|
||||||
|
onClick: () => setOpen(true),
|
||||||
|
},
|
||||||
|
React.createElement('span', { className: 'dsh-ua-avatar-icon' }, '🐋'),
|
||||||
|
props.wide ? React.createElement('span', { className: 'dsh-ua-avatar-label' }, 'API 用量') : null)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
slots.inject('shell.overlay', () => slots.register(
|
||||||
|
{ name: 'shell.overlay', id: 'dsh-usage-modal', order: 100 },
|
||||||
|
() => {
|
||||||
|
const open = useOpen()
|
||||||
|
if (!open) return null
|
||||||
|
return React.createElement(UsageModal, { onClose: () => setOpen(false) })
|
||||||
|
}
|
||||||
|
))
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// dsh-usage 插件 — Host 半
|
||||||
|
// 通过 cordis_define 定义(动态 Cordis 插件)。
|
||||||
|
// 提供两个 Package-private RPC:
|
||||||
|
// usage/balance — 读取 %DSH_HOME%\.credentials.yaml 的 DEEPSEEK_API_KEY,
|
||||||
|
// 调用 DeepSeek 官方余额 API(GET /user/balance),返回 JSON。
|
||||||
|
// usage/openPortal— 用系统默认浏览器打开官网用量页。
|
||||||
|
// 密钥只在 node 子进程内读取与使用,绝不打印、不入库。
|
||||||
|
return {
|
||||||
|
inject: ['shell'],
|
||||||
|
apply(ctx) {
|
||||||
|
harness.handle('usage/balance', async (args) => {
|
||||||
|
const script = [
|
||||||
|
"const fs = require('node:fs');",
|
||||||
|
"const os = require('node:os');",
|
||||||
|
"const path = require('node:path');",
|
||||||
|
"const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');",
|
||||||
|
"const file = path.join(home, '.credentials.yaml');",
|
||||||
|
"let key = null;",
|
||||||
|
"try { const txt = fs.readFileSync(file, 'utf8'); const m = txt.match(/DEEPSEEK_API_KEY:\\s*(\\S+)/); if (m) key = m[1]; } catch (e) {}",
|
||||||
|
"if (!key) { console.log(JSON.stringify({ error: 'no-api-key', file: file })); process.exit(0); }",
|
||||||
|
"fetch('https://api.deepseek.com/user/balance', { headers: { Authorization: 'Bearer ' + key, Accept: 'application/json' }, signal: AbortSignal.timeout(15000) })",
|
||||||
|
" .then(r => r.json().then(j => console.log(JSON.stringify(Object.assign({ status: r.status }, j)))))",
|
||||||
|
" .catch(e => console.log(JSON.stringify({ error: String((e && e.message) || e) })));",
|
||||||
|
].join('\n')
|
||||||
|
try {
|
||||||
|
const spec = ctx.shell.resolve({ command: 'node', args: ['-e', script] })
|
||||||
|
const res = await ctx.shell.run(spec)
|
||||||
|
const stdout = String(res.stdout || '')
|
||||||
|
const lines = stdout.trim().split(/\r?\n/).filter(Boolean)
|
||||||
|
if (lines.length) return JSON.parse(lines[lines.length - 1])
|
||||||
|
return { error: 'empty-output', stderr: String(res.stderr || '').slice(0, 400) }
|
||||||
|
} catch (e) {
|
||||||
|
return { error: String((e && e.message) || e) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
harness.handle('usage/openPortal', async () => {
|
||||||
|
try {
|
||||||
|
const spec = ctx.shell.resolve({ command: 'cmd', args: ['/c', 'start', '', 'https://platform.deepseek.com/usage'] })
|
||||||
|
await ctx.shell.run(spec)
|
||||||
|
return { ok: true }
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: String((e && e.message) || e) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// 下载 GitHub CLI (gh) 最新 Windows x64 便携版 zip。
|
||||||
|
// 用法: node dl-gh.mjs [输出目录] → 打印 { tag, file, bytes }
|
||||||
|
// 沙箱内 curl/winget 的 HTTPS 被禁,但 node fetch 可用,故用 node 下载。
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const outDir = path.resolve(process.argv[2] || '.')
|
||||||
|
fs.mkdirSync(outDir, { recursive: true })
|
||||||
|
|
||||||
|
const relUrl = 'https://api.github.com/repos/cli/cli/releases/latest'
|
||||||
|
const rel = await (await fetch(relUrl, { headers: { 'User-Agent': 'dsh-enhancements' }, signal: AbortSignal.timeout(30000) })).json()
|
||||||
|
if (!rel.tag_name) throw new Error('bad release response: ' + JSON.stringify(rel).slice(0, 200))
|
||||||
|
const asset = rel.assets.find((a) => a.name.endsWith('windows_amd64.zip'))
|
||||||
|
if (!asset) throw new Error('no windows_amd64.zip asset in ' + rel.tag_name)
|
||||||
|
|
||||||
|
const out = path.join(outDir, asset.name)
|
||||||
|
const res = await fetch(asset.browser_download_url, { signal: AbortSignal.timeout(300000) })
|
||||||
|
if (!res.ok) throw new Error('download failed: HTTP ' + res.status)
|
||||||
|
const buf = Buffer.from(await res.arrayBuffer())
|
||||||
|
fs.writeFileSync(out, buf)
|
||||||
|
console.log(JSON.stringify({ tag: rel.tag_name, file: out, bytes: buf.length }))
|
||||||
Reference in New Issue
Block a user