@caozeal/dsh-next-git 0.0.0-stage → 0.1.3
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 +169 -2
- package/cordis.patch.yml +3 -0
- package/icon.png +0 -0
- package/icon.svg +1 -0
- package/index.js +171 -0
- package/lib/client.js +1790 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +68 -4
- package/src/ai-commit.js +194 -0
- package/src/git.js +399 -0
- package/src/operations.js +510 -0
package/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-manager — Host 入口。
|
|
3
|
+
*
|
|
4
|
+
* 注册认证路由 `POST /api/git-manager`(由 connection 层做浏览器凭据校验),
|
|
5
|
+
* 把请求分发给 src/operations.js;git 进程统一走 ctx.subprocess,AI 提交信息
|
|
6
|
+
* 走可选的 llm + agentDefaultModel 服务(缺失时降级)。
|
|
7
|
+
*/
|
|
8
|
+
import { cleanGitEnv, GitError } from './src/git.js'
|
|
9
|
+
import { createOperations } from './src/operations.js'
|
|
10
|
+
|
|
11
|
+
export const name = 'git-manager'
|
|
12
|
+
|
|
13
|
+
/** connection 提供认证 fetch 路由;subprocess 执行 git。 */
|
|
14
|
+
export const inject = ['connection', 'subprocess']
|
|
15
|
+
|
|
16
|
+
const ROUTE_PATH = '/api/git-manager'
|
|
17
|
+
const STDOUT_MAX_BYTES = 8 * 1024 * 1024
|
|
18
|
+
const STDERR_MAX_BYTES = 1024 * 1024
|
|
19
|
+
|
|
20
|
+
/** 构造 JSON Response。 */
|
|
21
|
+
function jsonResponse(status, payload) {
|
|
22
|
+
return new Response(JSON.stringify(payload), {
|
|
23
|
+
status,
|
|
24
|
+
headers: {
|
|
25
|
+
'content-type': 'application/json; charset=utf-8',
|
|
26
|
+
'cache-control': 'no-store',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function errorPayload(error) {
|
|
32
|
+
const code = error instanceof GitError ? error.code : 'internal'
|
|
33
|
+
const raw = error && error.message ? String(error.message) : String(error)
|
|
34
|
+
const message = raw.length > 4000 ? `${raw.slice(0, 4000)}…` : raw
|
|
35
|
+
return { ok: false, error: { code, message } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 读取 subprocess 收集模式下的输出。 */
|
|
39
|
+
function readCollected(handle, which) {
|
|
40
|
+
try {
|
|
41
|
+
const reader = handle && handle.collected ? handle.collected[which] : undefined
|
|
42
|
+
if (!reader) return ''
|
|
43
|
+
return reader.readFrom(0).text ?? ''
|
|
44
|
+
} catch {
|
|
45
|
+
return ''
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 把 ctx.subprocess 包装成 operations 需要的 exec。 */
|
|
50
|
+
function createExec(ctx) {
|
|
51
|
+
return async function exec(argv, options = {}) {
|
|
52
|
+
const { cwd, signal, timeoutMs = 30000 } = options
|
|
53
|
+
const controller = new AbortController()
|
|
54
|
+
let timedOut = false
|
|
55
|
+
const onAbort = () => controller.abort()
|
|
56
|
+
if (signal) {
|
|
57
|
+
if (signal.aborted) controller.abort()
|
|
58
|
+
else signal.addEventListener('abort', onAbort, { once: true })
|
|
59
|
+
}
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
timedOut = true
|
|
62
|
+
controller.abort()
|
|
63
|
+
}, Math.max(100, timeoutMs))
|
|
64
|
+
try {
|
|
65
|
+
const handle = ctx.subprocess.spawn({
|
|
66
|
+
argv,
|
|
67
|
+
cwd,
|
|
68
|
+
stdio: {
|
|
69
|
+
stdin: 'ignore',
|
|
70
|
+
stdout: { maxBytes: STDOUT_MAX_BYTES },
|
|
71
|
+
stderr: { maxBytes: STDERR_MAX_BYTES },
|
|
72
|
+
},
|
|
73
|
+
graceMs: 3000,
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
env: cleanGitEnv(),
|
|
76
|
+
})
|
|
77
|
+
const outcome = await handle.done
|
|
78
|
+
const stdout = readCollected(handle, 'stdout')
|
|
79
|
+
const stderr = readCollected(handle, 'stderr')
|
|
80
|
+
if (timedOut || controller.signal.aborted) {
|
|
81
|
+
throw new GitError(`git 命令超时或被取消:${argv.join(' ')}`, {
|
|
82
|
+
code: 'timeout',
|
|
83
|
+
stdout,
|
|
84
|
+
stderr,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
return { code: outcome.exitCode, stdout, stderr }
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof GitError) throw error
|
|
90
|
+
if (timedOut || controller.signal.aborted) {
|
|
91
|
+
throw new GitError(`git 命令超时或被取消:${argv.join(' ')}`, { code: 'timeout' })
|
|
92
|
+
}
|
|
93
|
+
const message = error && error.message ? error.message : String(error)
|
|
94
|
+
const code = error && (error.code === 'ENOENT' || /\bENOENT\b/.test(message))
|
|
95
|
+
? 'cwd-unavailable'
|
|
96
|
+
: 'internal'
|
|
97
|
+
throw new GitError(
|
|
98
|
+
code === 'cwd-unavailable'
|
|
99
|
+
? `无法访问工作目录或 git 可执行文件(${cwd}):${message}`
|
|
100
|
+
: `无法执行 ${argv.join(' ')}:${message}`,
|
|
101
|
+
{ code },
|
|
102
|
+
)
|
|
103
|
+
} finally {
|
|
104
|
+
clearTimeout(timer)
|
|
105
|
+
if (signal) signal.removeEventListener('abort', onAbort)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 读取默认模型选择;任何异常都视为未配置。 */
|
|
111
|
+
function readSelection(ctx) {
|
|
112
|
+
try {
|
|
113
|
+
const service = ctx.get('agentDefaultModel')
|
|
114
|
+
if (!service || typeof service.currentSelection !== 'function') return null
|
|
115
|
+
return service.currentSelection() ?? null
|
|
116
|
+
} catch {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 处理一次 HTTP 请求。 */
|
|
122
|
+
async function handleRequest(request, operations) {
|
|
123
|
+
if (request.method !== 'POST') {
|
|
124
|
+
return jsonResponse(405, { ok: false, error: { code: 'invalid-params', message: '只支持 POST' } })
|
|
125
|
+
}
|
|
126
|
+
let body
|
|
127
|
+
try {
|
|
128
|
+
body = await request.json()
|
|
129
|
+
} catch {
|
|
130
|
+
return jsonResponse(400, { ok: false, error: { code: 'invalid-params', message: '请求体不是合法 JSON' } })
|
|
131
|
+
}
|
|
132
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
133
|
+
return jsonResponse(400, { ok: false, error: { code: 'invalid-params', message: '请求体必须是 JSON 对象' } })
|
|
134
|
+
}
|
|
135
|
+
const { op, ...params } = body
|
|
136
|
+
try {
|
|
137
|
+
const data = await operations.handle(op, params, request.signal)
|
|
138
|
+
return jsonResponse(200, { ok: true, data })
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// 参数错误(含未知 op)用 400;git 业务失败仍是 200 + ok:false,
|
|
141
|
+
// 让 UI 统一按 body 处理而不把错误码当网络故障。
|
|
142
|
+
const status = error instanceof GitError && error.code === 'invalid-params' ? 400 : 200
|
|
143
|
+
return jsonResponse(status, errorPayload(error))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Host 插件主体。
|
|
149
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
150
|
+
*/
|
|
151
|
+
export function apply(ctx) {
|
|
152
|
+
const operations = createOperations({
|
|
153
|
+
exec: createExec(ctx),
|
|
154
|
+
ai: () => ({
|
|
155
|
+
llm: ctx.get('llm'),
|
|
156
|
+
selection: readSelection(ctx),
|
|
157
|
+
}),
|
|
158
|
+
})
|
|
159
|
+
ctx.effect(() => {
|
|
160
|
+
const dispose = ctx.connection.fetch.register({
|
|
161
|
+
path: ROUTE_PATH,
|
|
162
|
+
methods: ['POST'],
|
|
163
|
+
requestBody: 'buffered',
|
|
164
|
+
fetch: (request) => handleRequest(request, operations),
|
|
165
|
+
})
|
|
166
|
+
return () => {
|
|
167
|
+
void dispose()
|
|
168
|
+
}
|
|
169
|
+
}, 'git-manager: /api/git-manager route')
|
|
170
|
+
console.log(`[git-manager] 已注册 ${ROUTE_PATH}`)
|
|
171
|
+
}
|