dsh-agentone 0.5.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 +104 -0
- package/cordis.patch.yml +5 -0
- package/lib/auth.js +111 -0
- package/lib/client.js +218 -0
- package/lib/http.js +41 -0
- package/lib/index.js +895 -0
- package/lib/model.js +200 -0
- package/lib/page.js +857 -0
- package/lib/proc.js +113 -0
- package/lib/skill.js +132 -0
- package/lib/store.js +60 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# dsh-agentone
|
|
2
|
+
|
|
3
|
+
AgentOne 平台集成插件(dsh web 版 / 桌面版通用)。安装后让独立 dsh 接入公司
|
|
4
|
+
AgentOne 平台:飞书登录、平台模型、SkillHub 技能、套餐申请、dsh 插件管理、
|
|
5
|
+
lark-cli / ccpg-cli 状态探测。
|
|
6
|
+
|
|
7
|
+
## 能力总览(M1–M4 已全部交付)
|
|
8
|
+
|
|
9
|
+
- **飞书登录(M1)**:设备登录流(一次性授权码),凭据保存在本机
|
|
10
|
+
`$DSH_HOME/agentone/credentials.json`(0600)。access 令牌临期自动轮换
|
|
11
|
+
(single-flight),refresh 失效自动回到未登录态。
|
|
12
|
+
- **平台模型(M2)**:登录后自动把 dsh 的 llm provider 指向 AgentOne
|
|
13
|
+
OpenAI 兼容代理(`/api/plugin/v1`),模型密钥不下发到本机;对话经平台
|
|
14
|
+
走 TokenHub 按用户计量。平台 403 的 `plan_required` 错误码原样透传,
|
|
15
|
+
前端据此引导申请套餐(不依赖中文文案匹配)。
|
|
16
|
+
- **SkillHub 技能(M3)**:套餐技能启动/登录时自动装齐;技能市场搜索、
|
|
17
|
+
手动安装/卸载;技能包 zip 走平台代理下载(穿越清洗 + 原子落盘)。
|
|
18
|
+
- **套餐申请(M3+)**:状态页「套餐」tab 展示当前套餐/审批中/可申请列表,
|
|
19
|
+
一键提交申请;审批通过后前端轮询检测到 active 自动同步模型。
|
|
20
|
+
- **dsh 插件管理(M3+)**:列出镜像内置与用户自装插件(版本/升级/激活态),
|
|
21
|
+
内置插件缺失时启动自动重装;自身随平台镜像升级(列表中不提供按钮)。
|
|
22
|
+
- **CLI 探测(M4)**:`lark-cli` / `ccpg-cli` 安装与授权态三态探测。
|
|
23
|
+
- **首启引导**:dsh 首次启动弹出飞书登录引导(order -50,早于官方
|
|
24
|
+
「添加 API Key」),登录一次即跳过官方密钥引导;暗色主题适配。
|
|
25
|
+
|
|
26
|
+
所有平台请求统一走 `lib/http.js`(默认 10s 超时、技能包下载 60s),
|
|
27
|
+
平台挂起时快速失败并给出中文提示,不会吊死状态页轮询。
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
dsh plugin --profile web add dsh-agentone
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
平台托管镜像集成:`docker/dsh/Dockerfile.default` 默认插件块(见
|
|
36
|
+
`plugins/README.md` 全流程)。
|
|
37
|
+
|
|
38
|
+
## 使用
|
|
39
|
+
|
|
40
|
+
打开 dsh web 的 `/agentone/` 页面(或 dsh 设置 → AgentOne)→「飞书登录」→
|
|
41
|
+
浏览器完成平台飞书 OAuth → 自动跳回并显示已登录账号。授权完成页会等待
|
|
42
|
+
模型配置就绪后自动关闭;配置失败时展示原因并提供「重试同步」。
|
|
43
|
+
|
|
44
|
+
登录成功后插件自动完成模型配置(也可点「重新同步模型」手动触发):
|
|
45
|
+
|
|
46
|
+
- `$DSH_HOME/settings.yaml` 写入 `llm-pi-ai.providers.platform`(指向平台
|
|
47
|
+
`/api/plugin/v1` 代理)与 `agent-default-model`,dsh 模型选择器出现
|
|
48
|
+
「平台模型」;
|
|
49
|
+
- `$DSH_HOME/.credentials.yaml` 写入 `refs.AGENTONE_API_KEY`(0600),内容
|
|
50
|
+
是 30 天插件令牌而非真实模型密钥——密钥不出平台,TokenHub 按用户计量;
|
|
51
|
+
- 只覆盖 `platform` 命名空间,用户自己配置的其它 provider 原样保留;
|
|
52
|
+
- 「退出登录」(页头按钮)会吊销平台令牌、移除凭据引用与 platform
|
|
53
|
+
provider(其它 provider 的默认模型设置不受影响);凭据文件损坏时也能
|
|
54
|
+
正常退出(坏文件移开为 `credentials.json.corrupt`)。
|
|
55
|
+
|
|
56
|
+
登录流(与平台 `app/api/plugin_auth.py` 契约):
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
插件页 ──POST /agentone/api/login──▶ 本插件后端
|
|
60
|
+
◀─ {login_url} ──
|
|
61
|
+
浏览器 ──login_url──▶ 平台飞书 OAuth ──▶ /api/auth/plugin/landing
|
|
62
|
+
landing(JS, Bearer) ──POST /api/auth/plugin/grant──▶ {redirect_url(一次性 code)}
|
|
63
|
+
浏览器 ──redirect_url──▶ 本插件 /agentone/callback?code
|
|
64
|
+
插件后端 ──POST /api/auth/plugin/exchange──▶ {access_token}
|
|
65
|
+
(存本机 0600,state 比对防 CSRF)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
所有写操作接口仅接受 `Content-Type: application/json` 的请求,阻止跨站
|
|
69
|
+
表单伪造 JSON POST。
|
|
70
|
+
|
|
71
|
+
## 配置(dsh settings)
|
|
72
|
+
|
|
73
|
+
| 字段 | 默认 | 说明 |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `platformUrl` | `https://ccpg.one.agentone.work` | AgentOne 平台地址(本地开发用 profile `cordis.patch.yml` 覆盖为 `http://127.0.0.1:8558`) |
|
|
76
|
+
| `storeDir` | `$DSH_HOME/agentone` | 凭据目录 |
|
|
77
|
+
|
|
78
|
+
密钥类值一律不进 settings,仅落 `credentials.json`(0600)。
|
|
79
|
+
|
|
80
|
+
## 模块结构
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
lib/
|
|
84
|
+
index.js 路由与业务编排(登录/状态/技能/套餐/插件/CLI)
|
|
85
|
+
page.js 状态页与授权完成页的 HTML/CSS/JS 模板(纯静态)
|
|
86
|
+
http.js 平台请求统一通道(超时、错误码透传)
|
|
87
|
+
auth.js 登录流、令牌轮换/吊销、能力面/套餐 API
|
|
88
|
+
model.js 平台模型 → settings.yaml / .credentials.yaml
|
|
89
|
+
skill.js 技能下载/解包/卸载(zip 穿越清洗、原子落盘)
|
|
90
|
+
store.js 凭据持久化(0600;损坏自动移开)
|
|
91
|
+
proc.js dsh/pnpm CLI 转发与 lark-cli/ccpg-cli 探测
|
|
92
|
+
client.js dsh 浏览器端 bundle(设置菜单 iframe + 首启引导)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 开发
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# 本地路径安装调试(无需发包)
|
|
99
|
+
dsh plugin --profile web add /path/to/plugins/dsh/dsh-agentone
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
无构建步骤(纯 ESM JS);运行时 peer 依赖 `@deepseek-ai/cordis` /
|
|
103
|
+
`@deepseek-ai/schemastery` 由 dsh host 提供。测试:`npm install && node --test`
|
|
104
|
+
(YAML 合并/登出清理语义、路由冒烟、凭据容错、令牌轮换等)。
|
package/cordis.patch.yml
ADDED
package/lib/auth.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// AgentOne 设备登录流(一次性授权码)。
|
|
2
|
+
//
|
|
3
|
+
// 流程(与平台 app/api/plugin_auth.py 契约对应):
|
|
4
|
+
// 1. buildLoginUrl:拼平台飞书 OAuth 入口,redirect 指向平台 landing 页并
|
|
5
|
+
// 携带 callback_url/state(经飞书 OAuth state 往返透传);
|
|
6
|
+
// 2. 浏览器完成飞书登录 → landing 页用平台令牌换一次性 code → 跳转
|
|
7
|
+
// callback_url(即本插件的 /agentone/callback 路由);
|
|
8
|
+
// 3. exchangeCode:code 换新签发的平台访问令牌。
|
|
9
|
+
import { randomBytes } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
import { PlatformError, platformError, platformFetch } from './http.js';
|
|
12
|
+
|
|
13
|
+
export function newLoginState() {
|
|
14
|
+
return randomBytes(16).toString('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildLoginUrl(platformUrl, callbackUrl, state) {
|
|
18
|
+
const landing = `/api/auth/plugin/landing?callback_url=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
|
|
19
|
+
const redirect = encodeURIComponent(landing);
|
|
20
|
+
return `${platformUrl.replace(/\/+$/, '')}/api/auth/feishu/login?redirect=${redirect}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function exchangeCode(platformUrl, code) {
|
|
24
|
+
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/auth/plugin/exchange`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: JSON.stringify({ code }),
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw await platformError(response, '授权码校验失败,请重新登录');
|
|
31
|
+
}
|
|
32
|
+
return response.json();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** refresh token 轮换:返回新 access/refresh;401 时 error.status=401(会话已死)。 */
|
|
36
|
+
export async function refreshPlatformToken(platformUrl, refreshToken) {
|
|
37
|
+
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/plugin/v1/token/refresh`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw await platformError(response, '刷新登录失败');
|
|
44
|
+
}
|
|
45
|
+
return response.json();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 登出吊销 refresh token(尽力而为,失败不阻断登出)。 */
|
|
49
|
+
export async function revokePlatformToken(platformUrl, refreshToken) {
|
|
50
|
+
try {
|
|
51
|
+
await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/plugin/v1/token/revoke`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
// 平台不可达时本地凭据仍会清除;下次登录会签发新 token
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 拉取平台能力面(开关/套餐);失败返回 null,调用方按能力未知处理。 */
|
|
62
|
+
export async function fetchPlatformCapabilities(platformUrl, accessToken) {
|
|
63
|
+
try {
|
|
64
|
+
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/plugin/v1/capabilities`, {
|
|
65
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) return null;
|
|
68
|
+
return await response.json();
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 用平台令牌调 /api/me 取账号信息;失败返回 null(令牌可能已过期)。 */
|
|
75
|
+
export async function fetchPlatformProfile(platformUrl, accessToken) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/me`, {
|
|
78
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) return null;
|
|
81
|
+
return await response.json();
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 套餐状态聚合(state ∈ none|pending|active|expired + 可申请套餐)。失败抛中文错误。 */
|
|
88
|
+
export async function fetchPlatformPlan(platformUrl, accessToken) {
|
|
89
|
+
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/plugin/v1/plan`, {
|
|
90
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw await platformError(response, '获取套餐状态失败');
|
|
94
|
+
}
|
|
95
|
+
return response.json();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 提交套餐申请;平台规则冲突(重复待审/已有套餐等)抛其中文错误。 */
|
|
99
|
+
export async function applyPlatformPlan(platformUrl, accessToken, planId, message) {
|
|
100
|
+
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/plugin/v1/plan/apply`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({ plan_id: planId, message }),
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw await platformError(response, '套餐申请提交失败');
|
|
107
|
+
}
|
|
108
|
+
return response.json();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { PlatformError };
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// dsh-agentone 浏览器端 bundle:设置菜单 + 首启引导。
|
|
2
|
+
//
|
|
3
|
+
// 两个 slot 注册(fiber inject 只声明短服务名 'slots'——写包名会让 web boot
|
|
4
|
+
// 挂起;包名级依赖在 package.json dsh.client.inject,两套语义不能混):
|
|
5
|
+
//
|
|
6
|
+
// settings.section 设置弹窗「AgentOne」区(iframe 嵌宿主 /agentone/ 页)
|
|
7
|
+
// settings.onboarding 首启引导步骤(order -50,早于官方「添加 API Key」
|
|
8
|
+
// 的 order 0)——平台用户登录一次,官方引导因已有可用
|
|
9
|
+
// provider 自动跳过,不再被要求填 DeepSeek 官方密钥。
|
|
10
|
+
//
|
|
11
|
+
// 引导组件运行在 dsh web 原生 origin(非 iframe),直接 fetch 插件路由;
|
|
12
|
+
// 登录在新标签页完成(顶层上下文,避开跨源 iframe localStorage 分区),
|
|
13
|
+
// 本组件轮询 /agentone/api/status,登录成功即 complete()。
|
|
14
|
+
window.__ModuleLoader__.load({
|
|
15
|
+
id: 'dsh-agentone',
|
|
16
|
+
factory: (require) => {
|
|
17
|
+
const React = require('react');
|
|
18
|
+
|
|
19
|
+
function AgentOneSection() {
|
|
20
|
+
return React.createElement('iframe', {
|
|
21
|
+
src: '/agentone/',
|
|
22
|
+
title: 'AgentOne',
|
|
23
|
+
style: {
|
|
24
|
+
border: 'none',
|
|
25
|
+
width: '100%',
|
|
26
|
+
height: '100%',
|
|
27
|
+
minHeight: '480px',
|
|
28
|
+
borderRadius: '12px',
|
|
29
|
+
background: 'transparent',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 暗色适配:跟随系统 prefers-color-scheme(dsh 主题跟随系统时口径一致)
|
|
35
|
+
const overlayStyle = {
|
|
36
|
+
position: 'fixed',
|
|
37
|
+
inset: '0',
|
|
38
|
+
zIndex: '60',
|
|
39
|
+
display: 'flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
justifyContent: 'center',
|
|
42
|
+
background: 'rgba(15, 18, 25, 0.55)',
|
|
43
|
+
fontFamily: 'system-ui, -apple-system, "PingFang SC", sans-serif',
|
|
44
|
+
};
|
|
45
|
+
const cardStyle = {
|
|
46
|
+
width: 'min(460px, calc(100vw - 48px))',
|
|
47
|
+
background: 'var(--agentone-card, #ffffff)',
|
|
48
|
+
color: 'var(--agentone-text, #1f2328)',
|
|
49
|
+
borderRadius: '16px',
|
|
50
|
+
padding: '28px 28px 24px',
|
|
51
|
+
boxShadow: '0 24px 64px rgba(15, 18, 25, 0.28)',
|
|
52
|
+
};
|
|
53
|
+
const titleStyle = { margin: '0 0 10px', fontSize: '19px', fontWeight: '600' };
|
|
54
|
+
const bodyStyle = {
|
|
55
|
+
margin: '0 0 20px',
|
|
56
|
+
fontSize: '14px',
|
|
57
|
+
lineHeight: '1.7',
|
|
58
|
+
color: 'var(--agentone-muted, #57606a)',
|
|
59
|
+
};
|
|
60
|
+
const primaryBtn = {
|
|
61
|
+
padding: '9px 22px',
|
|
62
|
+
fontSize: '14px',
|
|
63
|
+
borderRadius: '10px',
|
|
64
|
+
border: 'none',
|
|
65
|
+
cursor: 'pointer',
|
|
66
|
+
background: '#2f6feb',
|
|
67
|
+
color: '#fff',
|
|
68
|
+
marginRight: '10px',
|
|
69
|
+
};
|
|
70
|
+
const secondaryBtn = {
|
|
71
|
+
...primaryBtn,
|
|
72
|
+
background: 'var(--agentone-hover, #f2f3f5)',
|
|
73
|
+
color: 'var(--agentone-text, #3c434d)',
|
|
74
|
+
marginRight: '0',
|
|
75
|
+
};
|
|
76
|
+
// 变量注入一次:暗色下换一套卡片/文字颜色,亮色维持原值
|
|
77
|
+
let themeInjected = false;
|
|
78
|
+
function injectTheme() {
|
|
79
|
+
if (themeInjected) return;
|
|
80
|
+
themeInjected = true;
|
|
81
|
+
const style = document.createElement('style');
|
|
82
|
+
style.textContent = [
|
|
83
|
+
'@media (prefers-color-scheme: dark) { .agentone-onboarding-root {',
|
|
84
|
+
' --agentone-card: #161b22; --agentone-text: #e6edf3;',
|
|
85
|
+
' --agentone-muted: #9ea7b3; --agentone-hover: #21262d;',
|
|
86
|
+
'} }',
|
|
87
|
+
].join('');
|
|
88
|
+
document.head.appendChild(style);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function AgentOneOnboarding(props) {
|
|
92
|
+
const { complete } = props;
|
|
93
|
+
const [phase, setPhase] = React.useState('checking'); // checking | idle | waiting
|
|
94
|
+
const [error, setError] = React.useState('');
|
|
95
|
+
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
let cancelled = false;
|
|
98
|
+
(async () => {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch('/agentone/api/status');
|
|
101
|
+
const status = await response.json();
|
|
102
|
+
if (!cancelled && status?.logged_in) complete();
|
|
103
|
+
else if (!cancelled) setPhase('idle');
|
|
104
|
+
} catch {
|
|
105
|
+
if (!cancelled) setPhase('idle');
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [complete]);
|
|
112
|
+
|
|
113
|
+
const startLogin = async () => {
|
|
114
|
+
setError('');
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch('/agentone/api/login', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'Content-Type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ callback_url: `${location.origin}/agentone/callback` }),
|
|
120
|
+
});
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
setError(data.error || '发起登录失败');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
window.open(data.login_url, '_blank');
|
|
127
|
+
setPhase('waiting');
|
|
128
|
+
} catch {
|
|
129
|
+
setError('发起登录失败,请检查本机插件是否运行');
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
React.useEffect(() => {
|
|
134
|
+
if (phase !== 'waiting') return;
|
|
135
|
+
const timer = setInterval(async () => {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch('/agentone/api/status');
|
|
138
|
+
const status = await response.json();
|
|
139
|
+
if (status?.logged_in) {
|
|
140
|
+
clearInterval(timer);
|
|
141
|
+
complete();
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
/* 平台暂不可达时继续等 */
|
|
145
|
+
}
|
|
146
|
+
}, 2000);
|
|
147
|
+
return () => clearInterval(timer);
|
|
148
|
+
}, [phase, complete]);
|
|
149
|
+
|
|
150
|
+
if (phase === 'checking') return null;
|
|
151
|
+
|
|
152
|
+
injectTheme();
|
|
153
|
+
return React.createElement(
|
|
154
|
+
'div',
|
|
155
|
+
{ style: overlayStyle, className: 'agentone-onboarding-root' },
|
|
156
|
+
React.createElement(
|
|
157
|
+
'div',
|
|
158
|
+
{ style: cardStyle },
|
|
159
|
+
React.createElement('h2', { style: titleStyle }, '登录 AgentOne 平台'),
|
|
160
|
+
React.createElement(
|
|
161
|
+
'p',
|
|
162
|
+
{ style: bodyStyle },
|
|
163
|
+
phase === 'waiting'
|
|
164
|
+
? '已打开授权窗口,完成飞书授权后这里会自动继续…'
|
|
165
|
+
: '使用公司飞书账号登录,公司托管的模型与技能会自动配置,无需任何 API Key。',
|
|
166
|
+
),
|
|
167
|
+
error
|
|
168
|
+
? React.createElement('p', { style: { ...bodyStyle, color: '#c0392b' } }, error)
|
|
169
|
+
: null,
|
|
170
|
+
React.createElement(
|
|
171
|
+
'div',
|
|
172
|
+
null,
|
|
173
|
+
React.createElement(
|
|
174
|
+
'button',
|
|
175
|
+
{
|
|
176
|
+
style: phase === 'waiting' ? { ...primaryBtn, opacity: 0.7 } : primaryBtn,
|
|
177
|
+
onClick: startLogin,
|
|
178
|
+
disabled: phase === 'waiting',
|
|
179
|
+
},
|
|
180
|
+
phase === 'waiting' ? '等待授权…' : '飞书登录',
|
|
181
|
+
),
|
|
182
|
+
React.createElement(
|
|
183
|
+
'button',
|
|
184
|
+
{ style: secondaryBtn, onClick: complete },
|
|
185
|
+
'暂不登录',
|
|
186
|
+
),
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function apply(ctx) {
|
|
193
|
+
ctx.slots.inject('settings.section', () =>
|
|
194
|
+
ctx.slots.register(
|
|
195
|
+
{
|
|
196
|
+
name: 'settings.section',
|
|
197
|
+
id: 'agentone',
|
|
198
|
+
order: 90,
|
|
199
|
+
label: () => 'AgentOne',
|
|
200
|
+
},
|
|
201
|
+
AgentOneSection,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
ctx.slots.inject('settings.onboarding', () =>
|
|
205
|
+
ctx.slots.register(
|
|
206
|
+
{
|
|
207
|
+
name: 'settings.onboarding',
|
|
208
|
+
id: 'agentone-login',
|
|
209
|
+
order: -50,
|
|
210
|
+
},
|
|
211
|
+
AgentOneOnboarding,
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { name: 'agentone', inject: ['slots'], apply };
|
|
217
|
+
},
|
|
218
|
+
});
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// 平台请求统一通道:所有访问 AgentOne 平台的 fetch 都过这里。
|
|
2
|
+
// 之前各模块裸 fetch,平台一旦挂起,状态页 5s 轮询与启动同步链全部吊死;
|
|
3
|
+
// 这里统一挂超时中止(默认 10s,技能包下载单独放宽到 60s),并把平台
|
|
4
|
+
// 错误体的 code(如 plan_required)带回调用方,不再只留中文文案。
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
6
|
+
|
|
7
|
+
/** 平台错误:message 面向用户(中文),status/code 供调用方分支判断。 */
|
|
8
|
+
export class PlatformError extends Error {
|
|
9
|
+
constructor(message, { status = 0, code = null } = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'PlatformError';
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 从平台错误响应体解析错误。平台同时存在 FastAPI detail 形态与 OpenAI
|
|
19
|
+
* 兼容 error 形态(plugin_gateway),两种都认,取 message/code。
|
|
20
|
+
*/
|
|
21
|
+
export async function platformError(response, fallbackMessage) {
|
|
22
|
+
const data = await response.json().catch(() => ({}));
|
|
23
|
+
return new PlatformError(data?.detail || data?.error?.message || fallbackMessage, {
|
|
24
|
+
status: response.status,
|
|
25
|
+
code: data?.error?.code || data?.code || null,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 带超时的平台请求:超时/中止转成中文 PlatformError,其余原样抛出。 */
|
|
30
|
+
export async function platformFetch(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
31
|
+
let response;
|
|
32
|
+
try {
|
|
33
|
+
response = await fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs) });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
|
|
36
|
+
throw new PlatformError('连接 AgentOne 平台超时,请稍后重试');
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
return response;
|
|
41
|
+
}
|