@mu-cabin/muc-cli 0.5.5 → 0.6.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.
@@ -1,386 +0,0 @@
1
- #!/usr/bin/env bash
2
- # prep-repo — bare-cache + per-turn-worktree provisioner for the
3
- # code-review-gitlab assistant template.
4
- #
5
- # Modes:
6
- # --repo <url> --author-uid <UID> --chat-id <id> --msg-id <id>
7
- # [--branch-topic <topic>]
8
- # Create a fresh worktree on a new branch
9
- # muc-assistant/<topic> off origin/HEAD. If the branch exists, appends
10
- # -2, -3, ... . Member-checks <UID> at Developer+. Sets git author to
11
- # <UID> in the worktree. Writes the Muc-Assistant-Origin footer helper
12
- # to ../muc-assistant-origin.txt and prints the absolute worktree path
13
- # on stdout.
14
- #
15
- # --repo <url> --author-uid <UID> --reuse-branch <branch>
16
- # Same, but check out an existing branch (used by /refine to
17
- # continue an existing assistant-MR). Member-checks Developer+.
18
- #
19
- # --check-access <reporter|developer> --author-uid <UID> --repo <url>
20
- # Member-check only; no worktree. Prints "ok" on success.
21
- #
22
- # Cache root: ${MUC_ASSISTANT_REPO_CACHE:-$HOME/.cache/muc-assistant/repos}
23
- # The muc daemon sets MUC_ASSISTANT_REPO_CACHE to "<cwd>/.cache/repos"
24
- # so the bare-cache lives inside the per-turn workspace and is wiped
25
- # with the rest of the cwd on cleanup. Running this script outside the
26
- # daemon falls back to $HOME/.cache/muc-assistant/repos.
27
- # Worktree root for /implement /refine: $PWD/repo (the daemon's per-turn
28
- # workspace dir, wiped after the turn).
29
- #
30
- # Failure: anything not OK -> non-zero exit + human message on stderr.
31
- # The agent surfaces stderr verbatim back to the chat.
32
-
33
- set -euo pipefail
34
-
35
- CACHE_ROOT="${MUC_ASSISTANT_REPO_CACHE:-$HOME/.cache/muc-assistant/repos}"
36
-
37
- die() {
38
- printf 'prep-repo: %s\n' "$*" >&2
39
- exit 1
40
- }
41
-
42
- require_cmd() {
43
- command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1"
44
- }
45
-
46
- require_cmd git
47
- require_cmd glab
48
-
49
- usage() {
50
- cat >&2 <<'USAGE'
51
- usage:
52
- prep-repo --repo <url> --author-uid <UID> --chat-id <id> --msg-id <id> [--branch-topic <topic>]
53
- prep-repo --repo <url> --author-uid <UID> --reuse-branch <branch>
54
- prep-repo --check-access <reporter|developer> --author-uid <UID> --repo <url>
55
- USAGE
56
- exit 2
57
- }
58
-
59
- # --- arg parse -----------------------------------------------------------
60
- REPO=""
61
- AUTHOR_UID=""
62
- CHAT_ID=""
63
- MSG_ID=""
64
- BRANCH_TOPIC=""
65
- REUSE_BRANCH=""
66
- CHECK_ACCESS=""
67
-
68
- while [[ $# -gt 0 ]]; do
69
- case "$1" in
70
- --repo) REPO="${2:-}"; shift 2 ;;
71
- --author-uid) AUTHOR_UID="${2:-}"; shift 2 ;;
72
- --chat-id) CHAT_ID="${2:-}"; shift 2 ;;
73
- --msg-id) MSG_ID="${2:-}"; shift 2 ;;
74
- --branch-topic) BRANCH_TOPIC="${2:-}"; shift 2 ;;
75
- --reuse-branch) REUSE_BRANCH="${2:-}"; shift 2 ;;
76
- --check-access) CHECK_ACCESS="${2:-}"; shift 2 ;;
77
- -h|--help) usage ;;
78
- *) die "unknown arg: $1" ;;
79
- esac
80
- done
81
-
82
- [[ -n "$REPO" ]] || { usage; }
83
- [[ -n "$AUTHOR_UID" ]] || die "missing --author-uid"
84
-
85
- # --- repo url -> group/repo + host + cache slug -------------------------
86
- # Accept https://, http://, ssh://, git@host:path SSH-shorthand, and bare
87
- # "group/repo" slugs. When an explicit protocol prefix is present we *know*
88
- # the first segment after stripping it is the host, so strip it
89
- # unconditionally (heuristics on dot-vs-no-dot fail for short LAN hosts
90
- # like `git@gitlab:foo/bar`).
91
- #
92
- # Sets globals: HOST (empty when input was a bare slug), PROJECT, CLONE_REPO,
93
- # and REQUESTED_REF_PATH (best-effort branch/ref path from GitLab UI URLs).
94
- parse_repo_url() {
95
- local s="$1" had_host=0 ui_suffix=""
96
- HOST=""
97
- CLONE_REPO=""
98
- REQUESTED_REF_PATH=""
99
- s="${s%%#*}"
100
- s="${s%%\?*}"
101
- if [[ "$s" == https://* ]]; then s="${s#https://}"; had_host=1
102
- elif [[ "$s" == http://* ]]; then s="${s#http://}"; had_host=1
103
- elif [[ "$s" == ssh://* ]]; then s="${s#ssh://}"
104
- s="${s#*@}" # drop user@
105
- had_host=1
106
- elif [[ "$s" == git@* ]]; then s="${s#git@}"
107
- s="${s/://}" # host:path -> host/path
108
- had_host=1
109
- fi
110
- s="${s%.git}"
111
- if [[ "$s" == *"/-/"* ]]; then
112
- ui_suffix="${s#*/-/}"
113
- s="${s%%/-/*}" # drop GitLab UI suffix /-/...
114
- case "$ui_suffix" in
115
- tree/*) REQUESTED_REF_PATH="${ui_suffix#tree/}" ;;
116
- blob/*) REQUESTED_REF_PATH="${ui_suffix#blob/}" ;;
117
- raw/*) REQUESTED_REF_PATH="${ui_suffix#raw/}" ;;
118
- commits/*) REQUESTED_REF_PATH="${ui_suffix#commits/}" ;;
119
- esac
120
- REQUESTED_REF_PATH="${REQUESTED_REF_PATH%/}"
121
- fi
122
- if (( had_host )); then
123
- case "$s" in
124
- */*) HOST="${s%%/*}"; s="${s#*/}" ;;
125
- *) die "cannot parse repo (no path after host): $REPO" ;;
126
- esac
127
- fi
128
- s="${s%/}"
129
- case "$s" in
130
- */*) ;;
131
- *) die "expected <group>/<repo> in: $REPO" ;;
132
- esac
133
- PROJECT="$s"
134
- if (( had_host )); then
135
- case "$REPO" in
136
- https://*) CLONE_REPO="https://$HOST/$PROJECT.git" ;;
137
- http://*) CLONE_REPO="http://$HOST/$PROJECT.git" ;;
138
- ssh://*) CLONE_REPO="ssh://$HOST/$PROJECT.git" ;;
139
- git@*) CLONE_REPO="git@$HOST:$PROJECT.git" ;;
140
- *) CLONE_REPO="$PROJECT" ;;
141
- esac
142
- else
143
- CLONE_REPO="$PROJECT"
144
- fi
145
- }
146
-
147
- parse_repo_url "$REPO"
148
- SLUG="${PROJECT//\//__}"
149
- BARE="$CACHE_ROOT/$SLUG.git"
150
- # When the URL omitted a host we let glab fall back to its configured
151
- # default (typically the host the user `glab auth login`-ed against).
152
- glab_api() {
153
- if [[ -n "$HOST" ]]; then
154
- glab api --hostname "$HOST" "$@"
155
- else
156
- glab api "$@"
157
- fi
158
- }
159
-
160
-
161
- # --- glab encoding ------------------------------------------------------
162
- url_encode() {
163
- # %-encode every byte so subgroups (with /) and any special chars survive.
164
- local s="$1" out="" i c
165
- for ((i=0; i<${#s}; i++)); do
166
- c="${s:i:1}"
167
- case "$c" in
168
- [a-zA-Z0-9._~-]) out+="$c" ;;
169
- *) printf -v c '%%%02X' "'$c"; out+="$c" ;;
170
- esac
171
- done
172
- printf '%s' "$out"
173
- }
174
- PROJECT_ENC="$(url_encode "$PROJECT")"
175
-
176
- # --- uid resolution -----------------------------------------------------
177
- # `members/all/<id>` only accepts usernames on GitLab >= 15.4. Older
178
- # instances (e.g. gitlab.ceair.com) reject a username with HTTP 400
179
- # "user_id is invalid", which the access check would then mis-report as
180
- # "not a member". Resolve non-numeric AUTHOR_UID to a numeric user id via
181
- # `users?username=...` first so the membership query always uses an id
182
- # the server understands.
183
- resolve_uid() {
184
- local input="$1" enc body id
185
- if [[ "$input" =~ ^[0-9]+$ ]]; then
186
- printf '%s' "$input"
187
- return
188
- fi
189
- enc="$(url_encode "$input")"
190
- if ! body="$(glab_api "users?username=$enc" 2>/dev/null)"; then
191
- die "failed to resolve username '$input' via glab API (host: ${HOST:-default}; check token + network)"
192
- fi
193
- id="$(printf '%s' "$body" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n1)"
194
- if [[ -z "$id" ]]; then
195
- die "no GitLab user named '$input' on ${HOST:-the configured glab host}"
196
- fi
197
- printf '%s' "$id"
198
- }
199
-
200
- AUTHOR_UID_NUM="$(resolve_uid "$AUTHOR_UID")"
201
-
202
- # --- access level check -------------------------------------------------
203
- # `members/all/<numeric-id>` returns 200 + JSON containing `access_level`
204
- # (10=Guest 20=Reporter 30=Developer 40=Maintainer 50=Owner). 404 = not
205
- # a member. We pass --hostname when the input URL gave us a host;
206
- # otherwise glab falls back to its configured default host.
207
- access_level() {
208
- local uid="$1" body level
209
- if ! body="$(glab_api "projects/$PROJECT_ENC/members/all/$uid" 2>/dev/null)"; then
210
- printf '0'
211
- return
212
- fi
213
- level="$(printf '%s' "$body" | sed -n 's/.*"access_level"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n1)"
214
- printf '%s' "${level:-0}"
215
- }
216
-
217
- require_access() {
218
- local need="$1" lvl
219
- lvl="$(access_level "$AUTHOR_UID_NUM")"
220
- if (( lvl == 0 )); then
221
- die "$AUTHOR_UID (id=$AUTHOR_UID_NUM) is not a member of $PROJECT (or $PROJECT does not exist / glab token can't see it)"
222
- fi
223
- if (( lvl < need )); then
224
- die "$AUTHOR_UID has access_level=$lvl on $PROJECT, need >= $need"
225
- fi
226
- }
227
-
228
- # --- check-access mode --------------------------------------------------
229
- if [[ -n "$CHECK_ACCESS" ]]; then
230
- case "$CHECK_ACCESS" in
231
- reporter) require_access 20 ;;
232
- developer) require_access 30 ;;
233
- *) die "unknown --check-access value: $CHECK_ACCESS (expected reporter|developer)" ;;
234
- esac
235
- printf 'ok\n'
236
- exit 0
237
- fi
238
-
239
- # Both write modes require Developer+.
240
- require_access 30
241
-
242
- # --- bare cache: ensure + fetch ----------------------------------------
243
- mkdir -p "$CACHE_ROOT"
244
- if [[ ! -d "$BARE/refs" ]]; then
245
- # Initial mirror clone. Use the normalized project URL so GitLab UI links
246
- # (/-/tree, /-/blob, /-/merge_requests, ...) clone the repository, not the
247
- # web page URL. The host/protocol still comes from the user-supplied URL so
248
- # glab/git pick up the right stored credential helper.
249
- if ! git clone --bare --filter=blob:none "$CLONE_REPO" "$BARE" >&2; then
250
- die "git clone --bare failed for $CLONE_REPO -> $BARE"
251
- fi
252
- fi
253
-
254
- if ! git -C "$BARE" fetch --prune --quiet origin >&2; then
255
- die "git fetch failed in $BARE"
256
- fi
257
-
258
- # Reap dead worktree registrations from prior crashed turns so a new add
259
- # of the same branch name doesn't error out with "already checked out".
260
- git -C "$BARE" worktree prune --quiet 2>/dev/null || true
261
-
262
- # --- worktree --------------------------------------------------------------
263
- # Always create the worktree as a "repo/" subdir of the current cwd so the
264
- # daemon's per-turn workspace cleanup (rm -rf cwd) takes it with it.
265
- WT="$PWD/repo"
266
- [[ -e "$WT" ]] && rm -rf "$WT"
267
-
268
- if [[ -n "$REUSE_BRANCH" ]]; then
269
- # /refine path: check out an existing branch.
270
- if ! git -C "$BARE" worktree add --quiet "$WT" "$REUSE_BRANCH" >&2; then
271
- die "worktree add failed for branch $REUSE_BRANCH (does it exist on origin?)"
272
- fi
273
- else
274
- # /implement path: brand new branch off origin/HEAD.
275
- [[ -n "$CHAT_ID" ]] || die "missing --chat-id"
276
- [[ -n "$MSG_ID" ]] || die "missing --msg-id"
277
-
278
- # Sanitize the model-supplied topic into a compact, readable branch slug.
279
- sanitize_topic() {
280
- local s="$1"
281
- s="$(printf '%s' "$s" | LC_ALL=C tr '[:upper:]' '[:lower:]')"
282
- # Keep branch topics readable and portable: lowercase ascii + dashes.
283
- s="$(printf '%s' "$s" | LC_ALL=C tr -c 'a-z0-9' '-')"
284
- # Collapse runs of dashes.
285
- s="$(printf '%s' "$s" | sed -E 's/-+/-/g; s/^-//; s/-$//')"
286
- # Cap at 70 chars; suffixes like -2 are appended after this.
287
- s="${s:0:70}"
288
- s="$(printf '%s' "$s" | sed -E 's/-$//')"
289
- printf '%s' "${s:-code-change}"
290
- }
291
-
292
- branch_exists() {
293
- local branch="$1"
294
- git -C "$BARE" show-ref --verify --quiet "refs/heads/$branch" ||
295
- git -C "$BARE" show-ref --verify --quiet "refs/remotes/origin/$branch"
296
- }
297
-
298
- TOPIC_SAFE="$(sanitize_topic "$BRANCH_TOPIC")"
299
- BRANCH_BASE="muc-assistant/$TOPIC_SAFE"
300
- BRANCH="$BRANCH_BASE"
301
- for ((n = 2; n <= 99; n++)); do
302
- if ! branch_exists "$BRANCH"; then
303
- break
304
- fi
305
- BRANCH="$BRANCH_BASE-$n"
306
- done
307
- if branch_exists "$BRANCH"; then
308
- die "could not find a free branch name for $BRANCH_BASE after suffix -99"
309
- fi
310
-
311
- resolve_requested_ref() {
312
- [[ -n "$REQUESTED_REF_PATH" ]] || return 0
313
- if git -C "$BARE" rev-parse --verify --quiet "origin/$REQUESTED_REF_PATH^{commit}" >/dev/null; then
314
- printf 'origin/%s' "$REQUESTED_REF_PATH"
315
- return 0
316
- fi
317
- if git -C "$BARE" rev-parse --verify --quiet "$REQUESTED_REF_PATH^{commit}" >/dev/null; then
318
- printf '%s' "$REQUESTED_REF_PATH"
319
- return 0
320
- fi
321
-
322
- local ref best="" best_origin=""
323
- while IFS= read -r ref; do
324
- [[ "$ref" != HEAD ]] || continue
325
- case "$REQUESTED_REF_PATH" in
326
- "$ref"|"$ref"/*)
327
- if (( ${#ref} > ${#best_origin} )); then
328
- best_origin="$ref"
329
- fi
330
- ;;
331
- esac
332
- done < <(git -C "$BARE" for-each-ref --format='%(refname:strip=3)' refs/remotes/origin)
333
- if [[ -n "$best_origin" ]]; then
334
- printf 'origin/%s' "$best_origin"
335
- return 0
336
- fi
337
-
338
- while IFS= read -r ref; do
339
- case "$REQUESTED_REF_PATH" in
340
- "$ref"|"$ref"/*)
341
- if (( ${#ref} > ${#best} )); then
342
- best="$ref"
343
- fi
344
- ;;
345
- esac
346
- done < <(git -C "$BARE" for-each-ref --format='%(refname:strip=2)' refs/heads)
347
-
348
- if [[ -n "$best" ]]; then
349
- printf '%s' "$best"
350
- fi
351
- }
352
-
353
- # Prefer the branch encoded in a user-supplied GitLab URL (for example
354
- # /-/tree/<branch> or /-/blob/<branch>/path). Fall back to the project's
355
- # default branch only when the URL did not carry a resolvable branch.
356
- HEAD_REF="$(resolve_requested_ref)"
357
- if [[ -z "$HEAD_REF" ]]; then
358
- HEAD_REF="$(git -C "$BARE" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null || true)"
359
- HEAD_REF="${HEAD_REF#refs/remotes/}"
360
- fi
361
- if [[ -z "$HEAD_REF" ]]; then
362
- HEAD_REF="$(git -C "$BARE" symbolic-ref --quiet HEAD 2>/dev/null || true)"
363
- HEAD_REF="${HEAD_REF#refs/heads/}"
364
- fi
365
- if [[ -z "$HEAD_REF" ]]; then
366
- for candidate in origin/main main origin/master master; do
367
- if git -C "$BARE" rev-parse --verify --quiet "$candidate^{commit}" >/dev/null; then
368
- HEAD_REF="$candidate"
369
- break
370
- fi
371
- done
372
- fi
373
- [[ -n "$HEAD_REF" ]] || die "cannot resolve default branch for $PROJECT"
374
-
375
- if ! git -C "$BARE" worktree add --quiet -b "$BRANCH" "$WT" "$HEAD_REF" >&2; then
376
- die "worktree add -b $BRANCH failed (branch already exists?)"
377
- fi
378
-
379
- printf 'Muc-Assistant-Origin: chat:%s msg:%s\n' "$CHAT_ID" "$MSG_ID" > "$PWD/muc-assistant-origin.txt"
380
- fi
381
-
382
- # --- author identity on the worktree ------------------------------------
383
- git -C "$WT" config user.name "$AUTHOR_UID" >&2
384
- git -C "$WT" config user.email "${AUTHOR_UID}@CEAIR.COM" >&2
385
-
386
- printf '%s\n' "$WT"
@@ -1,63 +0,0 @@
1
- # <ASSISTANT_NAME> 安装提示
2
-
3
- 模板已经把以下文件落到本机:
4
-
5
- - 配置:`<MUC_ASSISTANT_DIR>/config.json`
6
- - 系统 prompt:`<MUC_ASSISTANT_DIR>/system-prompt.md`
7
- - 辅助脚本:`<MUC_ASSISTANT_BIN_DIR>/prep-repo`(已 chmod +x)
8
-
9
- 启动前请确认:
10
-
11
- 1. **glab 已登录到内网 GitLab**
12
- ```bash
13
- glab auth status
14
- # 期望看到 gitlab.ceair.com 已登录、token 有效。
15
- # 如未登录:glab auth login --hostname gitlab.ceair.com
16
- ```
17
- 助手会复用本机 glab 凭据 push 分支与开 MR;MR 在 GitLab 上显示
18
- "Opened by <你>",但每个 commit 的 author 是触发它的 MUC 用户
19
- (例如 `WANGLULU8 <WANGLULU8@CEAIR.COM>`)。
20
-
21
- 2. **裸仓库缓存目录**(首次运行会自动创建,无需手动)
22
- 守护进程把 `MUC_ASSISTANT_REPO_CACHE` 指向当前 turn 的工作目录
23
- (`~/.muc/assistants/<ASSISTANT_NAME>/workspace/turn-<msg-id>/.cache/repos`),
24
- prep-repo 落在这里,turn 结束随工作目录一起被清理。
25
- 不需要再维护 `~/.cache/muc-assistant/repos/` —— 已不再使用。
26
-
27
- 3. **触发人是目标仓库成员**
28
- glab 的登录身份始终是助手所属的机器人账号,但触发人可以是任何人。
29
- 助手会用 inbound 消息里的 MUC UID 调 `prep-repo --check-access` 查
30
- GitLab 项目成员列表,只要触发人在项目成员里就放行;不在成员里则把
31
- prep-repo 的 stderr 原样回给用户。
32
-
33
- 启动(先确保守护进程在运行):
34
-
35
- ```bash
36
- # 第一次:先把守护进程拉起来(前台进程,建议放进 tmux/screen)。
37
- muc assistantd-start
38
-
39
- # 然后把这个助手挂到守护进程上:
40
- muc assistant-start <ASSISTANT_NAME>
41
- ```
42
-
43
- 停止:
44
-
45
- ```bash
46
- muc assistant-stop <ASSISTANT_NAME> # 摘掉这个助手
47
- # 守护进程仍在运行;要彻底退出再:
48
- muc assistantd-stop
49
- ```
50
-
51
- 修改 prompt:
52
-
53
- ```bash
54
- muc assistant-edit <ASSISTANT_NAME>
55
- ```
56
-
57
- 测试触发(在 MUC 里 @ 助手账号):
58
-
59
- ```
60
- /code http://gitlab.ceair.com/<group>/<repo>/-/merge_requests/<n>
61
- /code http://gitlab.ceair.com/<group>/<repo> 把 X 加上 Y 的检查
62
- /code 把刚才那个改成可选 # 引用上一条助手回的 MR 通知,或在同一会话里直接接着提
63
- ```
@@ -1,153 +0,0 @@
1
- # Persona: <ASSISTANT_NAME> — GitLab Code Handler
2
-
3
- 你是 <ASSISTANT_NAME>,一个处理 GitLab 上各种代码任务的助手。回复用中文(除非
4
- 用户主动用英文)。
5
-
6
- ## 唯一入口
7
-
8
- ```
9
- /code [<gitlab-url>] [<描述>]
10
- ```
11
-
12
- 只处理 gitlab 代码相关的问题,与代码无关也不是对你之前消息的有效追问的,一律 skip。
13
-
14
- ## 你要做什么
15
-
16
- 根据 URL 种类(MR / 项目 / Issue / Commit / File blob / ...)和用户描述,
17
- 自己判断这次是评审、实现、改 MR、解释代码、答疑还是别的什么;动手前用一句话
18
- 说出你的判断,别反复跟用户来回确认。
19
-
20
- ## 触发人识别
21
-
22
- inbound 消息以 `[NEW ... ] from <UID>: <text>` 开头,`<UID>` 是触发人的公司
23
- 账号(如 `WANGLULU8`)。它用于:
24
-
25
- 1. GitLab 成员校验。
26
- 2. commit 作者(由 `prep-repo --author-uid <UID>` 写入 worktree 的 git config)。
27
-
28
- 解析不出 UID 就停下,回复「无法识别触发人,请联系管理员」。
29
-
30
- ## URL 解析顺序
31
-
32
- URL 不要求出现在当前消息里。按顺序找:
33
-
34
- 1. 当前消息里的完整 `http(s)://gitlab.ceair.com/...` URL。
35
- 2. quote 引用:inbound 把回复渲染为 `↪ quote #N ... : <preview>`,从 preview 抽 URL。
36
- 3. 同一会话最近一次你自己回出去的 GitLab URL(在你的上下文里)。多个时优先 MR URL。
37
- 4. 都没有 → 回复「请贴一下 GitLab URL,或引用我之前的 MR 通知」并停止。
38
-
39
- ## commit message 前缀(凡是要新建 commit 都要有)
40
-
41
- 只要这次任务会落 commit(任何要 push 代码的活),commit message 必须带前缀。
42
- 常见两种:
43
-
44
- - `#ignore_scan#` —— 跳过代码扫描门禁的默认前缀。
45
- - JIRA故事号,例如 `DO23187-1977` —— 跟到具体需求 / Bug 单上。
46
-
47
- 按下面顺序确定,**确定不下来就先问**,别擅自挑一个:
48
-
49
- 1. 用户这次消息里直接写明了(如「commit 用 DO23187-1977」、「按 ignore_scan
50
- 提就行」),用它。
51
- 2. 用户消息里能明显看出工单号(形如 `[A-Z]{2,}\d+(-\d+)?`),用它。
52
- 3. 同一会话里你之前已经问过 / 用户已经回过一次:直接复用,别重复问。
53
- 4. 都没有:在动手前先一句话问清楚(默认 `#ignore_scan#` 还是有JIRA故事号),
54
- 等用户回了再继续 prep-repo / 改代码。
55
-
56
- 最终 commit 形如 `<前缀> <动词> <简述>`,例如:
57
-
58
- ```
59
- #ignore_scan# feat(login): add captcha refresh
60
- DO23187-1977 fix(api): null-check session before sign
61
- ```
62
-
63
- 前缀只放第一行,body 里别再重复。凡是新建 commit,commit body/footer 必须带
64
- 一行 `Muc-Assistant-Origin: chat:<chat-id> msg:<msg-id>`;新分支模式下
65
- `prep-repo` 会把这行写到 `../muc-assistant-origin.txt`,直接复用它。
66
-
67
- ## 工具
68
-
69
- ### `$MUC_ASSISTANT_BIN_DIR/prep-repo` — 仓库脏活统一入口
70
-
71
- 做四件事:成员/access_level 校验、维护 per-turn 裸仓库缓存、创建或复用
72
- worktree、把触发人写进 worktree 的 git config。建 worktree 时 stdout 是
73
- worktree 绝对路径;`cd` 进去就能改代码、commit、push。
74
-
75
- 要不要建 worktree、建哪种,由你判断:纯评审 / 解释 / 答疑通常只需要
76
- `glab` 读一读,加一个 `--check-access` 就够了,别为了改一行不改的活
77
- 也开一个 worktree。要落 commit 时默认开**全新分支**;只有在用户明确要求
78
- 继续某个已有 MR / source_branch 时,才用 `--reuse-branch`,不限制分支是否
79
- 由助手创建。其他写代码场景一律新建 `muc-assistant/<topic>` 分支。
80
-
81
- ```bash
82
- # 仅查权限
83
- "$MUC_ASSISTANT_BIN_DIR/prep-repo" --check-access reporter \
84
- --author-uid <UID> --repo <url>
85
-
86
- # 全新分支(默认的写模式)
87
- "$MUC_ASSISTANT_BIN_DIR/prep-repo" --repo <url> \
88
- --author-uid <UID> --chat-id <chat-id> --msg-id <msg-id> \
89
- --branch-topic <english-kebab-topic>
90
-
91
- # 接着已有 assistant MR 的分支
92
- "$MUC_ASSISTANT_BIN_DIR/prep-repo" --repo <mr-url> \
93
- --author-uid <UID> --reuse-branch <source_branch>
94
- ```
95
-
96
- `--repo` 既能吃 project URL,也能吃 MR / issue / commit URL(自动剥到 project)。
97
- 如果用户贴的是 branch / file URL(如 `/-/tree/<branch>`、`/-/blob/<branch>/...`),
98
- 新分支会从该 URL 指向的 branch 派生;没有可解析 branch 时才从项目默认分支派生。
99
- chat-id:group 是 `[NEW group=<TID>]` 里的 `<TID>`;p2p 是 `dm-with-<UID>`。
100
- msg-id:prompt 没给就用 `t-<unix秒>`。
101
- branch-topic:从用户需求提炼 2-7 个英文单词,kebab-case,优先包含工单号
102
- (如 `do23187-add-guide-link` / `fix-login-timeout`)。脚本会生成
103
- `muc-assistant/<topic>`;同名已存在时自动用 `-2` / `-3` 后缀。
104
-
105
- 不要跳过 prep-repo 自己 git clone —— 成员校验和作者设置都在里面。
106
- 成员/access_level 校验**永远走 `prep-repo --check-access`**。
107
-
108
- ### `glab` — GitLab CLI(已登录)
109
-
110
- 所有 GitLab 操作**只走 `glab` 一线子命令**(`glab mr ...` / `glab issue ...` /
111
- `glab repo ...` 等)。**不要**直接打 GitLab REST API:不写 `glab api /...`,
112
- 也不 `curl https://gitlab.ceair.com/api/v4/...`。bot 账号不是管理员,
113
- `/users/:id/projects`、members 列表这类接口本来就拿不到完整结果,硬查只会
114
- 误导用户。
115
-
116
- ```bash
117
- glab mr view <mr-url> -F json
118
- glab mr diff <mr-url>
119
- glab mr note <mr-url> -m "<text>"
120
- glab mr create --draft --fill -d "<desc>" # 必须在 worktree 里跑
121
- glab issue view <issue-url> -F json
122
- glab repo view <project-url>
123
- ```
124
-
125
- push 到已有 MR 的 source_branch 后 MR 自动更新;新分支开 Draft MR 用
126
- `glab mr create --draft`,MR 描述里也要包含同一行 origin footer:
127
- `Muc-Assistant-Origin: chat:<chat-id> msg:<msg-id>`。不要跑 `npm install` /
128
- `npm test`,CI 会跑。
129
-
130
- ### 写代码 / 读代码
131
-
132
- worktree 里随便用 Read / Edit / Write / Grep / Bash。
133
-
134
- ## 安全 / 拒绝条件
135
-
136
- - 任何 muc 链路的写操作(send / send-file / withdraw / login / init /
137
- assistantd-* / assistant-* …)一律不准用(base prompt 已声明)。
138
- - 触发人不是项目成员:把 prep-repo 的报错原文返给用户,不要尝试绕过。
139
- - 不关闭/重开 issue,不 approve MR,不 force-push,不去 resolve 别人的
140
- 讨论 thread。
141
- - 拿不准、做不了:直说,别糊弄。
142
-
143
- ## 回复
144
-
145
- 回复内容由你自己根据做了什么决定:
146
-
147
- - 实现/修改完成:贴 MR URL,必要时附一两句改了什么。
148
- - 评审完成:「已在 MR 留 N 条评论:<mr-url>」或「看完了,没有需要改的」。
149
- - 解释 / 答疑:直接给答案,引用具体文件 / 行号。
150
- - 拿不准、做不了、被拒:说清楚原因。
151
-
152
- 不要为了凑话术重复用户已经知道的东西。失败原因(prep-repo 报错、glab
153
- 报错等)也走正常回复,不要把异常 trace 直接吐到 chat。
@@ -1,15 +0,0 @@
1
- {
2
- "id": "code-handler",
3
- "description": "GitLab 代码助手:一个 /code 命令处理评审 / 实现 / 修改 / 解释 / 答疑等所有代码相关任务,由助手根据 URL 类型与上下文自行决定动作与回复形态。授权基于 GitLab 项目成员身份;commit author 取自触发人 UID。",
4
- "backend": "claude-code",
5
- "model": "claude-sonnet-4-6",
6
- "commands": [
7
- { "name": "/code", "desc": "处理一个 GitLab 相关的代码任务(评审 / 实现 / 修改 / 解释 / 答疑等,由助手判断)", "usage": "/code [<gitlab-url>] [<描述>]" }
8
- ],
9
- "placeholders": ["ASSISTANT_NAME", "USER_NAME", "MUC_ASSISTANT_DIR", "MUC_ASSISTANT_BIN_DIR"],
10
- "prereqs": [
11
- "本机已完成 `glab auth login`,且已登录到 gitlab.ceair.com(运行 `glab auth status` 确认)",
12
- "本机有可用的 `git`",
13
- "触发人是目标 GitLab 项目的成员(glab 用助手的机器人账号登录;触发人由 inbound 的 MUC UID 解析,prep-repo 只校验其在项目成员列表里)"
14
- ]
15
- }
@@ -1,64 +0,0 @@
1
- # <ASSISTANT_NAME> 安装提示
2
-
3
- 模板已经把以下文件落到本机:
4
-
5
- - 配置:`<MUC_ASSISTANT_DIR>/config.json`
6
- - 系统 prompt:`<MUC_ASSISTANT_DIR>/system-prompt.md`
7
-
8
- 启动前请确认:
9
-
10
- 1. **后端 CLI 已登录可用**
11
-
12
- 模板填了一个默认的 `backend` / `model`,只是图省事;任何 muc 支持的
13
- 后端都能跑通用聊天。想换:
14
-
15
- ```bash
16
- muc assistant-new <ASSISTANT_NAME> --template general-chat \
17
- --backend <claude-code|codex|gemini|cursor> \
18
- --model <型号>
19
- ```
20
-
21
- 或事后改 `<MUC_ASSISTANT_DIR>/config.json` 里的 `backend` / `model`
22
- 字段。不论选哪个,先确认它已安装、已登录(按各自的登录命令操作),
23
- 再把这个助手挂上去。
24
-
25
- 2. **想让它操作在线文档 / 外部系统?先在后端里把对应能力配好**
26
-
27
- 助手不会自己注册外部凭据,全靠所选后端已有的 skill / MCP / 插件提供能力。
28
- 要让它能查飞书云文档、读知识库、操作 GitLab、访问公司内网 API 之类的场景,
29
- 先在所选后端里把对应技能 / MCP server / 插件配好;没配的话助手会回一句
30
- "本机后端没装这个能力"。各后端如何添加 skill / MCP,请查各自官方文档。
31
-
32
- 3. **作为兜底接收者**
33
-
34
- 本模板的 `commands: []` —— 助手不绑任何 `/cmd`,会接住会话里**所有**非
35
- `/help` / `/status` 且没被其他助手认领的消息。建议把它的 `priority` 设
36
- 高一些(数值越大越靠后),让 `/code`、`/image` 之类的专用助手先抢命令。
37
-
38
- 启动(先确保守护进程在运行):
39
-
40
- ```bash
41
- muc assistantd-start
42
- muc assistant-start <ASSISTANT_NAME>
43
- ```
44
-
45
- 停止:
46
-
47
- ```bash
48
- muc assistant-stop <ASSISTANT_NAME> # 摘掉这个助手
49
- muc assistantd-stop # 退守护进程
50
- ```
51
-
52
- 修改 prompt:
53
-
54
- ```bash
55
- muc assistant-edit <ASSISTANT_NAME>
56
- ```
57
-
58
- 测试触发(在 MUC 里 @ 助手账号):
59
-
60
- ```
61
- 帮我看看 [文件] 写了啥
62
- 飞书云文档 https://... 里第三章总结一下
63
- 把刚才那段 Python 的异常处理改成 try/except 的标准写法
64
- ```