gitee-ai-employee 1.0.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/LICENSE +21 -0
- package/README.md +93 -0
- package/README.zh.md +80 -0
- package/cordis.patch.yml +9 -0
- package/dsh/client.js +424 -0
- package/dsh/index.js +1004 -0
- package/package.json +56 -0
- package/preset/gitee-worker/agent.cordis.yml +66 -0
- package/preset/gitee-worker/preset.yml +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wangbobo-coder
|
|
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,93 @@
|
|
|
1
|
+
# Gitee AI 员工 (gitee-ai-employee)
|
|
2
|
+
|
|
3
|
+
An issue-driven AI developer for **DeepSeek Harness**, working against **Gitee (码云)**.
|
|
4
|
+
|
|
5
|
+
Mention your bot in a Gitee issue, and the plugin clones the repository, dispatches an
|
|
6
|
+
AI **worker agent** to implement the change, pushes a branch, opens a Pull Request to the
|
|
7
|
+
branch you ask for, and can auto-merge and auto-close the issue.
|
|
8
|
+
|
|
9
|
+
> 中文说明见 [README.zh.md](README.zh.md)。
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- **Polling**: periodically scans the Gitee issues of the repositories you watch for a new
|
|
14
|
+
mention of your bot (`@botName`).
|
|
15
|
+
- **AI development**: creates a worker agent in a profile that clones the repo (via
|
|
16
|
+
PowerShell + `curl` on Windows), explores it, implements the issue, runs checks, and
|
|
17
|
+
commits on a `ai-fix/issue-<number>` branch.
|
|
18
|
+
- **Branch-aware**: it reads branch instructions written in natural language and uses that
|
|
19
|
+
branch as both the development base and the PR target:
|
|
20
|
+
- `需要修改 master-sctbc 分支` / `修改 X 分支` / `改 X 分支`
|
|
21
|
+
- `基于 X 分支` / `在 X 分支上` / `以 X 分支为基础`
|
|
22
|
+
- `合并到 X` / `合到 X` / `目标分支 X` / `base: X`
|
|
23
|
+
The requested branch is validated against the remote with `git ls-remote`; if it does not
|
|
24
|
+
exist, the plugin falls back to the repository default branch and says so in a comment.
|
|
25
|
+
- **PR & close**: pushes the branch, creates a PR (base = the chosen branch), optionally
|
|
26
|
+
auto-merges it (`autoMerge`), and optionally closes the issue on success (`autoCloseIssue`).
|
|
27
|
+
- **Webhook / manual trigger**: optional webhook endpoint and a manual trigger URL for
|
|
28
|
+
instant processing without waiting for the poll.
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- DeepSeek Harness (dsh) 0.1.x, Windows (the plugin drives `git` and the Gitee API through
|
|
33
|
+
PowerShell + `C:\Windows\System32\curl.exe`), a Gitee account with a personal access token.
|
|
34
|
+
- The worker preset is **bundled and auto-installed** into `$DSH_HOME/.agent-presets/gitee-worker`
|
|
35
|
+
on first load — no manual setup.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
dsh plugin --profile <your-profile> add gitee-ai-employee
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
(or if you prefer installing straight from GitHub: `dsh plugin --profile <p> add github:wangbobo-coder/gitee-ai-employee`)
|
|
44
|
+
|
|
45
|
+
Restart dsh, then open **Settings → Plugins → Plugin configuration**, find the
|
|
46
|
+
**Gitee AI 员工** card and configure:
|
|
47
|
+
|
|
48
|
+
| Setting | Meaning |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `giteeToken` | Gitee personal access token (kept secret; never returned by the status API) |
|
|
51
|
+
| `botName` | The account/issues mention that triggers the bot, e.g. `gitee-ai`. ASCII is recommended. |
|
|
52
|
+
| `workRoot` | Where repositories are cloned; empty = `$DSH_HOME/gitee-workers` |
|
|
53
|
+
| `watchRepos` | Repos to poll, one `owner/repo` per line |
|
|
54
|
+
| `pollEnabled` / `pollIntervalMs` | Poll on/off and interval (milliseconds) |
|
|
55
|
+
| `autoMerge` | Auto-merge the created PR |
|
|
56
|
+
| `autoCloseIssue` | Close the issue once the task succeeds |
|
|
57
|
+
| `workerPreset` | Agent preset id used for the worker (default `gitee-worker`) |
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
1. Make sure `pollEnabled` is on (or use the manual trigger URL shown in the status API).
|
|
62
|
+
2. Open (or edit) an issue in a watched repo and mention your bot:
|
|
63
|
+
```
|
|
64
|
+
@gitee-ai
|
|
65
|
+
需要修改 master-sctbc 分支
|
|
66
|
+
参会人员选择增加学生选项,注意回显处理。
|
|
67
|
+
```
|
|
68
|
+
3. The plugin picks it up (usually within one poll interval), comments "已接单", runs the
|
|
69
|
+
worker, and later posts the result with the PR link. With `autoCloseIssue` on, the issue
|
|
70
|
+
is closed on success.
|
|
71
|
+
|
|
72
|
+
> The bot name match uses a lookahead (`(?![A-Za-z0-9_])`) so both ASCII and CJK bot names
|
|
73
|
+
> trigger correctly.
|
|
74
|
+
|
|
75
|
+
## Security notes
|
|
76
|
+
|
|
77
|
+
- User configuration is persisted to the **user's own profile patch layer**; the shipped
|
|
78
|
+
`cordis.patch.yml` contains only an empty default and never a token.
|
|
79
|
+
- The token is marked secret in the schema and is never echoed by the status/config API
|
|
80
|
+
(only a `tokenConfigured` boolean).
|
|
81
|
+
- The plugin runs with the same privileges as your dsh process; its worker agent gets
|
|
82
|
+
`danger-full-access` inside the cloned workspace only.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
- `dsh/index.js` — host plugin (Cordis object plugin with `dsh.bundle` manifest).
|
|
87
|
+
- `dsh/client.js` — web client half (settings card hover/status).
|
|
88
|
+
- `preset/gitee-worker/` — the bundled worker agent preset, auto-copied to
|
|
89
|
+
`$DSH_HOME/.agent-presets/gitee-worker` on load if missing.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Gitee AI 员工 (gitee-ai-employee)
|
|
2
|
+
|
|
3
|
+
面向 **DeepSeek Harness** 的 issue 驱动型 AI 开发者,对接 **Gitee(码云)**。
|
|
4
|
+
|
|
5
|
+
在 Gitee issue 里 @ 你的机器人,插件就会克隆仓库、派出 AI **worker agent** 实现改动、
|
|
6
|
+
推送分支、按你指定的分支创建 Pull Request,并可按需自动合并、自动关闭 issue。
|
|
7
|
+
|
|
8
|
+
## 功能
|
|
9
|
+
|
|
10
|
+
- **轮询**:定时扫描你监听的仓库里新出现 `@botName` 的 issue。
|
|
11
|
+
- **AI 开发**:创建 worker agent(Windows 下经 PowerShell + curl 克隆仓库),探索代码、
|
|
12
|
+
实现需求、跑校验、在 `ai-fix/issue-<number>` 分支上提交。
|
|
13
|
+
- **分支感知**:会读取 issue 里用自然语言指定的分支,作为开发基准和 PR 目标:
|
|
14
|
+
- `需要修改 master-sctbc 分支` / `修改 X 分支` / `改 X 分支`
|
|
15
|
+
- `基于 X 分支` / `在 X 分支上` / `以 X 分支为基础`
|
|
16
|
+
- `合并到 X` / `合到 X` / `目标分支 X` / `base: X`
|
|
17
|
+
指定分支会用 `git ls-remote` 校验远端存在性;不存在则退回仓库默认分支并在评论中说明。
|
|
18
|
+
- **PR 与关闭**:推送分支、创建 PR(base = 所选分支),可按需自动合并(`autoMerge`)、
|
|
19
|
+
成功后自动关闭 issue(`autoCloseIssue`)。
|
|
20
|
+
- **Webhook / 手动触发**:可选 webhook 端点与手动触发 URL,无需等轮询即时处理。
|
|
21
|
+
|
|
22
|
+
## 环境要求
|
|
23
|
+
|
|
24
|
+
- DeepSeek Harness (dsh) 0.1.x,Windows(插件通过 PowerShell + `C:\Windows\System32\curl.exe`
|
|
25
|
+
驱动 git 与 Gitee API),Gitee 账号及个人访问令牌。
|
|
26
|
+
- worker 预设在包内**自带并在首次加载时自动安装**到 `$DSH_HOME/.agent-presets/gitee-worker`,
|
|
27
|
+
无需手动配置。
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
dsh plugin --profile <你的-profile> add gitee-ai-employee
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
(或直接从 GitHub 安装:`dsh plugin --profile <p> add github:wangbobo-coder/gitee-ai-employee`)
|
|
36
|
+
|
|
37
|
+
重启 dsh 后,打开 **设置 → 插件 → 插件配置**,找到 **Gitee AI 员工** 卡片进行配置:
|
|
38
|
+
|
|
39
|
+
| 配置项 | 说明 |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `giteeToken` | Gitee 个人访问令牌(标记为 secret,状态接口不回显) |
|
|
42
|
+
| `botName` | 触发机器人的账号,如 `gitee-ai`。建议使用 ASCII 名称。 |
|
|
43
|
+
| `workRoot` | 仓库克隆目录;留空 = `$DSH_HOME/gitee-workers` |
|
|
44
|
+
| `watchRepos` | 监听仓库,每行一个 `owner/repo` |
|
|
45
|
+
| `pollEnabled` / `pollIntervalMs` | 是否轮询 / 轮询间隔(毫秒) |
|
|
46
|
+
| `autoMerge` | 是否自动合并创建的 PR |
|
|
47
|
+
| `autoCloseIssue` | 任务成功后是否自动关闭 issue |
|
|
48
|
+
| `workerPreset` | worker 使用的 agent 预设 id(默认 `gitee-worker`) |
|
|
49
|
+
|
|
50
|
+
## 使用
|
|
51
|
+
|
|
52
|
+
1. 打开 `pollEnabled`(或使用状态接口给出的手动触发 URL)。
|
|
53
|
+
2. 在监听的仓库里新建/编辑 issue 并 @ 机器人:
|
|
54
|
+
```
|
|
55
|
+
@gitee-ai
|
|
56
|
+
需要修改 master-sctbc 分支
|
|
57
|
+
参会人员选择增加学生选项,注意回显处理。
|
|
58
|
+
```
|
|
59
|
+
3. 插件会在一个轮询周期内接单(评论“已接单”),运行 worker,最后回帖结果与 PR 链接;
|
|
60
|
+
开启 `autoCloseIssue` 时成功后自动关闭 issue。
|
|
61
|
+
|
|
62
|
+
> 机器人匹配使用前瞻断言(`(?![A-Za-z0-9_])`),ASCII 与中文 botName 都能正确触发。
|
|
63
|
+
|
|
64
|
+
## 安全说明
|
|
65
|
+
|
|
66
|
+
- 用户配置持久化在**用户自身 profile 的 patch 层**;随包分发的 `cordis.patch.yml`
|
|
67
|
+
只含空默认,绝不包含 token。
|
|
68
|
+
- token 在 schema 中标记为 secret,状态/配置接口只返回 `tokenConfigured` 布尔值,不回显明文。
|
|
69
|
+
- 插件与你 dsh 进程同权限运行;worker agent 仅在克隆的工作区内获得 `danger-full-access`。
|
|
70
|
+
|
|
71
|
+
## 开发
|
|
72
|
+
|
|
73
|
+
- `dsh/index.js` — 宿主插件(标准 Cordis 对象插件,声明 `dsh.bundle` manifest)。
|
|
74
|
+
- `dsh/client.js` — Web 客户端半体(设置卡片/状态展示)。
|
|
75
|
+
- `preset/gitee-worker/` — 内置 worker agent 预设,首次加载缺失时自动复制到
|
|
76
|
+
`$DSH_HOME/.agent-presets/gitee-worker`。
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/dsh/client.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// Browser half of the Gitee AI employee dsh plugin: a settings card in the
|
|
2
|
+
// installed-plugins list (设置 → 已安装插件). The card reuses the exact
|
|
3
|
+
// PluginCard.module.css class names injected by @deepseek-ai/dsh-client-ui
|
|
4
|
+
// -settings-plugins (YyYd_a_*), so it renders pixel-identical to the built-in
|
|
5
|
+
// Shell / Agent loop / Web search cards — a border, a 12px radius, a big bold
|
|
6
|
+
// name header with a description, a collapsing body of fields, and a
|
|
7
|
+
// discard/save footer.
|
|
8
|
+
//
|
|
9
|
+
// Hand-written in the lazy-CJS bundle protocol (window.__ModuleLoader__.load
|
|
10
|
+
// with a factory returning cordis-plugin exports), so no build step and no
|
|
11
|
+
// imports from dsh client packages — the same zero-dependency stance as the
|
|
12
|
+
// host half. `slots` is optional (registered lazily via ctx.inject), so a
|
|
13
|
+
// non-web profile simply never renders the card.
|
|
14
|
+
window.__ModuleLoader__.load({
|
|
15
|
+
id: 'gitee-ai-employee',
|
|
16
|
+
factory: (require) => {
|
|
17
|
+
var module = { exports: {} }
|
|
18
|
+
var exports = module.exports
|
|
19
|
+
|
|
20
|
+
function labels() {
|
|
21
|
+
return {
|
|
22
|
+
title: 'Gitee AI 员工',
|
|
23
|
+
description: 'issue @员工 → AI 开发 → PR → 自动关闭',
|
|
24
|
+
expand: '展开',
|
|
25
|
+
collapse: '收起',
|
|
26
|
+
unsaved: '未保存',
|
|
27
|
+
loading: '加载中…',
|
|
28
|
+
loadFailed: '配置加载失败',
|
|
29
|
+
discard: '放弃',
|
|
30
|
+
save: '保存',
|
|
31
|
+
saving: '保存中…',
|
|
32
|
+
savedDone: '保存完成,已生效',
|
|
33
|
+
readOnly: '只读',
|
|
34
|
+
token: 'Gitee 私人令牌',
|
|
35
|
+
tokenHint: '留空保持原值',
|
|
36
|
+
bot: '机器人账号',
|
|
37
|
+
botHint: 'issue 里 @ 它才触发',
|
|
38
|
+
workRoot: '仓库工作目录',
|
|
39
|
+
repos: '监听仓库',
|
|
40
|
+
reposHint: '每行一个 owner/repo',
|
|
41
|
+
poll: '启用定时轮询',
|
|
42
|
+
interval: '轮询间隔(毫秒)',
|
|
43
|
+
merge: '自动合并 PR',
|
|
44
|
+
close: '成功后自动关闭 issue',
|
|
45
|
+
status: '查看任务状态',
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ConfigCard(react, ui) {
|
|
50
|
+
var h = react.createElement
|
|
51
|
+
var Input = ui && ui.Input
|
|
52
|
+
var Chevron = ui && ui.IconChevronDownOutline14
|
|
53
|
+
|
|
54
|
+
// 复用内置 settings-plugins 注入的字段样式类(At1oFq_*)
|
|
55
|
+
var fieldRow = (label, hint, control, key, groupName) =>
|
|
56
|
+
h(
|
|
57
|
+
groupName ? 'div' : 'label',
|
|
58
|
+
{
|
|
59
|
+
key: key,
|
|
60
|
+
role: groupName ? 'group' : undefined,
|
|
61
|
+
'aria-label': groupName || undefined,
|
|
62
|
+
className: 'At1oFq_field',
|
|
63
|
+
style: { display: 'flex', flexDirection: 'column', gap: '6px', padding: '12px 0' },
|
|
64
|
+
},
|
|
65
|
+
h('div', { className: 'At1oFq_head' },
|
|
66
|
+
h('span', { className: 'At1oFq_label' }, label),
|
|
67
|
+
hint
|
|
68
|
+
? h('span', { className: 'At1oFq_badgeMuted', style: { fontSize: '11px', lineHeight: '17px', color: 'var(--dsw-alias-label-tertiary)' } }, hint)
|
|
69
|
+
: null,
|
|
70
|
+
),
|
|
71
|
+
control,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// 自定义开关行:左侧标签,右侧紧凑的 iOS 风格 toggle
|
|
75
|
+
var toggleRow = (label, hint, checked, onChange, key) =>
|
|
76
|
+
h(
|
|
77
|
+
'div',
|
|
78
|
+
{
|
|
79
|
+
key: key,
|
|
80
|
+
className: 'At1oFq_field',
|
|
81
|
+
style: { display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '10px 0' },
|
|
82
|
+
},
|
|
83
|
+
h('div', { style: { minWidth: 0, display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
84
|
+
h('span', {
|
|
85
|
+
className: 'At1oFq_label',
|
|
86
|
+
style: { margin: 0, fontSize: '13px', fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
|
|
87
|
+
}, label),
|
|
88
|
+
hint
|
|
89
|
+
? h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)', lineHeight: 1.4 } }, hint)
|
|
90
|
+
: null,
|
|
91
|
+
),
|
|
92
|
+
h(Switch, { checked: !!checked, onChange, label }),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// 紧凑圆形开关(轨道 + 滑块),用 DSH 主题 token 着色
|
|
96
|
+
function Switch(props) {
|
|
97
|
+
var on = !!props.checked
|
|
98
|
+
return h(
|
|
99
|
+
'button',
|
|
100
|
+
{
|
|
101
|
+
type: 'button',
|
|
102
|
+
role: 'switch',
|
|
103
|
+
'aria-checked': on,
|
|
104
|
+
'aria-label': props.label,
|
|
105
|
+
onClick: () => props.onChange(!on),
|
|
106
|
+
style: {
|
|
107
|
+
appearance: 'none',
|
|
108
|
+
flex: 'none',
|
|
109
|
+
width: '36px',
|
|
110
|
+
height: '20px',
|
|
111
|
+
borderRadius: '999px',
|
|
112
|
+
border: 0,
|
|
113
|
+
padding: 0,
|
|
114
|
+
cursor: 'pointer',
|
|
115
|
+
position: 'relative',
|
|
116
|
+
background: on
|
|
117
|
+
? 'var(--dsw-alias-brand-primary, #2563eb)'
|
|
118
|
+
: 'var(--dsw-alias-bg-module-platform, rgba(127,127,127,0.35))',
|
|
119
|
+
transition: 'background .16s',
|
|
120
|
+
outline: 'none',
|
|
121
|
+
boxShadow: 'inset 0 0 0 1px rgba(127,127,127,0.25)',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
h('span', {
|
|
125
|
+
style: {
|
|
126
|
+
position: 'absolute',
|
|
127
|
+
top: '2px',
|
|
128
|
+
left: on ? '18px' : '2px',
|
|
129
|
+
width: '16px',
|
|
130
|
+
height: '16px',
|
|
131
|
+
borderRadius: '999px',
|
|
132
|
+
background: '#fff',
|
|
133
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.35)',
|
|
134
|
+
transition: 'left .16s',
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 平衡:onChange 里用 ref 读最新 draft,避免闭包陈旧
|
|
141
|
+
var draftRef = { current: null }
|
|
142
|
+
|
|
143
|
+
return function GiteeCard() {
|
|
144
|
+
var t = labels()
|
|
145
|
+
var openState = react.useState(false)
|
|
146
|
+
var summaryState = react.useState(null)
|
|
147
|
+
var draftState = react.useState(null)
|
|
148
|
+
var busyState = react.useState(false)
|
|
149
|
+
var noteState = react.useState('')
|
|
150
|
+
var failedState = react.useState(false)
|
|
151
|
+
var dirtyState = react.useState(false)
|
|
152
|
+
var open = openState[0]
|
|
153
|
+
var summary = summaryState[0]
|
|
154
|
+
var draft = draftState[0]
|
|
155
|
+
var busy = busyState[0]
|
|
156
|
+
var note = noteState[0]
|
|
157
|
+
var failed = failedState[0]
|
|
158
|
+
var dirty = dirtyState[0]
|
|
159
|
+
|
|
160
|
+
draftRef.current = draft
|
|
161
|
+
|
|
162
|
+
var seedDraft = (config) => ({
|
|
163
|
+
botName: (config && config.botName) || '',
|
|
164
|
+
workRoot: (config && config.workRoot) || '',
|
|
165
|
+
watchRepos: ((config && config.watchRepos) || []).join('\n'),
|
|
166
|
+
pollEnabled: !!(config && config.pollEnabled),
|
|
167
|
+
pollIntervalMs: (config && config.pollIntervalMs) || 60000,
|
|
168
|
+
autoMerge: !!(config && config.autoMerge),
|
|
169
|
+
autoCloseIssue: !!(config && config.autoCloseIssue),
|
|
170
|
+
tokenConfigured: !!(config && config.tokenConfigured),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
var load = react.useCallback(() => {
|
|
174
|
+
fetch('/gitee-ai/config')
|
|
175
|
+
.then((r) => r.json().then((body) => {
|
|
176
|
+
if (!r.ok) throw new Error(body.error || 'load failed')
|
|
177
|
+
return body
|
|
178
|
+
}))
|
|
179
|
+
.then((body) => {
|
|
180
|
+
summaryState[1](body.config)
|
|
181
|
+
var d = seedDraft(body.config)
|
|
182
|
+
draftRef.current = d
|
|
183
|
+
draftState[1](d)
|
|
184
|
+
noteState[1]('')
|
|
185
|
+
failedState[1](false)
|
|
186
|
+
})
|
|
187
|
+
.catch((error) => {
|
|
188
|
+
noteState[1](String(error.message ? error.message : error))
|
|
189
|
+
failedState[1](true)
|
|
190
|
+
})
|
|
191
|
+
}, [])
|
|
192
|
+
|
|
193
|
+
react.useEffect(() => {
|
|
194
|
+
if (open && summary === null) load()
|
|
195
|
+
}, [open, summary, load])
|
|
196
|
+
|
|
197
|
+
var setDraft = (d) => { draftState[1](d); dirtyState[1](true) }
|
|
198
|
+
var setDraftFromControl = (patch) => {
|
|
199
|
+
var d = Object.assign({}, draftRef.current, patch)
|
|
200
|
+
draftRef.current = d
|
|
201
|
+
draftState[1](d)
|
|
202
|
+
dirtyState[1](true)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
var save = () => {
|
|
206
|
+
if (!draft) return
|
|
207
|
+
busyState[1](true)
|
|
208
|
+
failedState[1](false)
|
|
209
|
+
noteState[1]('')
|
|
210
|
+
var payload = {
|
|
211
|
+
botName: draft.botName,
|
|
212
|
+
workRoot: draft.workRoot,
|
|
213
|
+
watchRepos: (draft.watchRepos || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean),
|
|
214
|
+
pollEnabled: draft.pollEnabled,
|
|
215
|
+
pollIntervalMs: Number(draft.pollIntervalMs) || 60000,
|
|
216
|
+
autoMerge: draft.autoMerge,
|
|
217
|
+
autoCloseIssue: draft.autoCloseIssue,
|
|
218
|
+
}
|
|
219
|
+
fetch('/gitee-ai/config', {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
body: JSON.stringify(payload),
|
|
223
|
+
})
|
|
224
|
+
.then((r) => r.json().then((body) => {
|
|
225
|
+
if (!r.ok) throw new Error(body.error || 'save failed')
|
|
226
|
+
return body
|
|
227
|
+
}))
|
|
228
|
+
.then((body) => {
|
|
229
|
+
noteState[1](t.savedDone)
|
|
230
|
+
summaryState[1](body.config)
|
|
231
|
+
var d = seedDraft(body.config)
|
|
232
|
+
draftRef.current = d
|
|
233
|
+
draftState[1](d)
|
|
234
|
+
dirtyState[1](false)
|
|
235
|
+
})
|
|
236
|
+
.catch((error) => {
|
|
237
|
+
noteState[1](String(error.message ? error.message : error))
|
|
238
|
+
failedState[1](true)
|
|
239
|
+
})
|
|
240
|
+
.finally(() => busyState[1](false))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
var discard = () => {
|
|
244
|
+
if (!summary) return
|
|
245
|
+
var d = seedDraft(summary)
|
|
246
|
+
draftRef.current = d
|
|
247
|
+
draftState[1](d)
|
|
248
|
+
dirtyState[1](false)
|
|
249
|
+
noteState[1]('')
|
|
250
|
+
failedState[1](false)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── 头部(大标题 + 描述 + 展开箭头)──
|
|
254
|
+
var statusLine = summary
|
|
255
|
+
? '轮询' + (summary.pollEnabled ? ' 开 · ' + Math.round((summary.pollIntervalMs || 0) / 1000) + 's' : ' 关')
|
|
256
|
+
+ ' · 仓库 ' + ((summary.watchRepos || []).length) + ' 个'
|
|
257
|
+
+ (summary.tokenConfigured ? '' : ' · 未配置令牌')
|
|
258
|
+
: t.description
|
|
259
|
+
|
|
260
|
+
var chevronEl = Chevron
|
|
261
|
+
? h(Chevron, {
|
|
262
|
+
className: 'YyYd_a_chevron' + (open ? ' YyYd_a_chevronOpen' : ''),
|
|
263
|
+
})
|
|
264
|
+
: h('span', {
|
|
265
|
+
className: 'YyYd_a_chevron' + (open ? ' YyYd_a_chevronOpen' : ''),
|
|
266
|
+
style: { display: 'inline-flex' },
|
|
267
|
+
}, '▾')
|
|
268
|
+
|
|
269
|
+
var header = h(
|
|
270
|
+
'button',
|
|
271
|
+
{
|
|
272
|
+
type: 'button',
|
|
273
|
+
className: 'YyYd_a_header',
|
|
274
|
+
'aria-expanded': open,
|
|
275
|
+
'aria-label': (open ? t.collapse : t.expand) + ': ' + t.title,
|
|
276
|
+
onClick: () => openState[1](!open),
|
|
277
|
+
},
|
|
278
|
+
h('span', { className: 'YyYd_a_headText' },
|
|
279
|
+
h('span', { className: 'YyYd_a_name' }, t.title),
|
|
280
|
+
h('span', { className: 'YyYd_a_description' }, statusLine),
|
|
281
|
+
),
|
|
282
|
+
dirty ? h('span', { className: 'YyYd_a_pending' }, t.unsaved) : null,
|
|
283
|
+
chevronEl,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
// ── 主体(字段 + 底部保存)──
|
|
287
|
+
var body = null
|
|
288
|
+
if (open) {
|
|
289
|
+
if (!summary || !draft) {
|
|
290
|
+
body = h(
|
|
291
|
+
'div',
|
|
292
|
+
{ className: 'YyYd_a_body', style: { padding: '12px 16px', color: 'var(--dsw-alias-label-tertiary)', fontSize: '13px' } },
|
|
293
|
+
note || t.loading,
|
|
294
|
+
)
|
|
295
|
+
} else {
|
|
296
|
+
var controls = []
|
|
297
|
+
controls.push(fieldRow(t.token, draft.tokenConfigured ? t.tokenHint : undefined, h(Input, {
|
|
298
|
+
key: 'token',
|
|
299
|
+
type: 'password',
|
|
300
|
+
placeholder: draft.tokenConfigured ? '••••••••' : '输入 Gitee 私人令牌',
|
|
301
|
+
value: (draft.token !== undefined ? draft.token : ''),
|
|
302
|
+
onChange: (e) => setDraftFromControl({ token: e.target.value }),
|
|
303
|
+
}), 'token'))
|
|
304
|
+
controls.push(fieldRow(t.bot, t.botHint, h(Input, {
|
|
305
|
+
key: 'bot',
|
|
306
|
+
value: draft.botName || '',
|
|
307
|
+
onChange: (e) => setDraftFromControl({ botName: e.target.value }),
|
|
308
|
+
}), 'bot'))
|
|
309
|
+
controls.push(fieldRow(t.workRoot, undefined, h(Input, {
|
|
310
|
+
key: 'workRoot',
|
|
311
|
+
value: draft.workRoot || '',
|
|
312
|
+
onChange: (e) => setDraftFromControl({ workRoot: e.target.value }),
|
|
313
|
+
}), 'workRoot'))
|
|
314
|
+
controls.push(fieldRow(t.repos, t.reposHint, h('textarea', {
|
|
315
|
+
key: 'repos',
|
|
316
|
+
rows: 3,
|
|
317
|
+
className: 'At1oFq_input',
|
|
318
|
+
style: { height: 'auto', padding: '8px 12px', resize: 'vertical', lineHeight: 1.5 },
|
|
319
|
+
value: draft.watchRepos || '',
|
|
320
|
+
onChange: (e) => setDraftFromControl({ watchRepos: e.target.value }),
|
|
321
|
+
}), 'repos'))
|
|
322
|
+
controls.push(toggleRow(t.poll, undefined, draft.pollEnabled, (v) => setDraftFromControl({ pollEnabled: v }), 'poll'))
|
|
323
|
+
controls.push(fieldRow(t.interval, undefined, h(Input, {
|
|
324
|
+
key: 'interval',
|
|
325
|
+
type: 'number',
|
|
326
|
+
value: draft.pollIntervalMs,
|
|
327
|
+
onChange: (e) => setDraftFromControl({ pollIntervalMs: e.target.value }),
|
|
328
|
+
}), 'interval'))
|
|
329
|
+
controls.push(toggleRow(t.merge, undefined, draft.autoMerge, (v) => setDraftFromControl({ autoMerge: v }), 'merge'))
|
|
330
|
+
controls.push(toggleRow(t.close, undefined, draft.autoCloseIssue, (v) => setDraftFromControl({ autoCloseIssue: v }), 'close'))
|
|
331
|
+
|
|
332
|
+
body = h(
|
|
333
|
+
'div',
|
|
334
|
+
{ className: 'YyYd_a_body' },
|
|
335
|
+
h('a', {
|
|
336
|
+
href: '/gitee-ai/settings',
|
|
337
|
+
target: '_blank',
|
|
338
|
+
rel: 'noreferrer',
|
|
339
|
+
style: { display: 'inline-block', marginTop: '12px', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)', textDecoration: 'underline' },
|
|
340
|
+
}, t.status + ' ↗'),
|
|
341
|
+
controls,
|
|
342
|
+
note
|
|
343
|
+
? h('p', {
|
|
344
|
+
className: failed ? 'YyYd_a_failed' : 'YyYd_a_readOnly',
|
|
345
|
+
role: 'status',
|
|
346
|
+
style: failed ? {} : { margin: '8px 0 0', fontSize: '12px', lineHeight: '1.5' },
|
|
347
|
+
}, note)
|
|
348
|
+
: null,
|
|
349
|
+
h('div', { className: 'YyYd_a_footer' },
|
|
350
|
+
h('button', {
|
|
351
|
+
type: 'button',
|
|
352
|
+
className: 'YyYd_a_discard',
|
|
353
|
+
disabled: !dirty || busy,
|
|
354
|
+
onClick: discard,
|
|
355
|
+
}, t.discard),
|
|
356
|
+
h('button', {
|
|
357
|
+
type: 'button',
|
|
358
|
+
className: 'YyYd_a_save',
|
|
359
|
+
disabled: busy,
|
|
360
|
+
onClick: save,
|
|
361
|
+
}, busy ? t.saving : t.save),
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return h(
|
|
368
|
+
'li',
|
|
369
|
+
{ className: 'YyYd_a_card' + (open ? ' YyYd_a_cardOpen' : '') },
|
|
370
|
+
header,
|
|
371
|
+
body,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function registerCard(ctx) {
|
|
377
|
+
if (typeof ctx.inject !== 'function') return
|
|
378
|
+
ctx.inject(['slots'], (scope) => {
|
|
379
|
+
fetch('/gitee-ai/config')
|
|
380
|
+
.then((response) => {
|
|
381
|
+
if (response.status === 404) return
|
|
382
|
+
try {
|
|
383
|
+
mountCard(scope)
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error('[gitee-ai] settings card skipped: ' + error)
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
.catch(() => {})
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function mountCard(ctx) {
|
|
393
|
+
var react
|
|
394
|
+
try {
|
|
395
|
+
react = require('react')
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error('[gitee-ai] settings card skipped: ' + error)
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
var ui
|
|
401
|
+
try {
|
|
402
|
+
ui = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
403
|
+
} catch (error) {
|
|
404
|
+
ui = {}
|
|
405
|
+
}
|
|
406
|
+
var Card = ConfigCard(react, ui)
|
|
407
|
+
ctx.slots.inject('settings.plugin.item', function* () {
|
|
408
|
+
yield ctx.slots.register({ name: 'settings.plugin.item', id: 'gitee-ai-employee', key: 'gitee-ai-employee', order: 30 }, Card)
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function apply(ctx) {
|
|
413
|
+
registerCard(ctx)
|
|
414
|
+
if (typeof ctx.effect === 'function') {
|
|
415
|
+
ctx.effect(() => () => {}, 'gitee-ai: settings card')
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
exports.apply = apply
|
|
420
|
+
// `slots` is optional, so it is not required here: registerCard checks.
|
|
421
|
+
exports.inject = []
|
|
422
|
+
return module.exports
|
|
423
|
+
},
|
|
424
|
+
})
|