修复:管理员复制他人 API Key 失败(新增 admin key 读取端点并接入前端 adminMode) - #11
Conversation
- 后端:移除 256KiB 采集上限,请求/响应体全量入库(MySQL 列已为 LONGTEXT) - 前端:新增 format-body,Claude SSE 流重组为等价非流式消息(text/thinking/signature/tool_use), 其他 SSE 解析为数组,截断 JSON 重新缩进分行展示 - 测试:vitest 9 例覆盖格式化各分支;capture writer 增加大 body 不截断回归测试 Signed-off-by: AGENTS (opencode)
- 会话识别:请求消息原始字节构建 SHA256 前缀哈希链,token 作用域内链值匹配即同一会话, 只存增量消息(new_messages),响应仍全量存储;compact/编辑重发自然开新会话 - 数据模型:chat_sessions(链头/轮次/消息数/system) + chat_turns(增量+响应); master 迁移 AutoMigrate 两表并 DROP 旧 chat_logs;MySQL system 保留字列反引号处理 - 提取:OpenAI chat/Claude/Gemini 规范化候选链,其余格式单轮会话;同会话并行写竞争按 v1 接受 - 管理 API:GET /api/chat_logs/sessions[/:id];列表不含消息体 - 前端:会话列表 + 转录时间线,跳过每轮回显的 assistant/model 消息,响应经 formatChatLogBody 重组展示(并入此前 OpenAI SSE 分块合并为 chat.completion) - 测试:Go service/model/controller 四包 + 前端 vitest 17 例;i18n 7 语言 Signed-off-by: AGENTS (opencode)
Signed-off-by: AGENTS
There was a problem hiding this comment.
🟡 Changes recommended
The new chat-log session/turn persistence introduces concurrency and memory-risk issues (no transactional locking and unbounded capture) and includes a potentially destructive DB migration (DROP TABLE) that needs an explicit migration/rollout decision.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds admin-capable API key plaintext retrieval endpoints and wires the frontend to use them in adminMode, while also refactoring chat-log storage and the admin chat-log UI from “per request log rows” into “sessions + turns” with a transcript viewer and improved request/response body formatting.
Changes:
- Add admin endpoints to fetch single/batch token keys and integrate frontend key resolution with
adminMode. - Replace chat log persistence/schema and admin APIs/UI with
chat_sessions+chat_turns, including transcript display and SSE/JSON body formatting utilities + tests. - Update i18n locale keys used by the new chat-log UI.
File summaries
| File | Description |
|---|---|
| web/src/routes/_authenticated/chat-logs/index.tsx | Adds user_id to URL search schema for chat sessions filtering. |
| web/src/i18n/locales/en.json | Adds new chat-log UI keys; removes unused keys. |
| web/src/i18n/locales/zh.json | Adds new chat-log UI keys; removes unused keys. |
| web/src/i18n/locales/zh-TW.json | Adds new chat-log UI keys; removes unused keys. |
| web/src/i18n/locales/fr.json | Adds new chat-log UI keys; removes unused keys. |
| web/src/i18n/locales/ja.json | Adds new chat-log UI keys; removes unused keys. |
| web/src/i18n/locales/ru.json | Adds new chat-log UI keys; removes unused keys. |
| web/src/i18n/locales/vi.json | Adds new chat-log UI keys; removes unused keys. |
| web/src/features/keys/components/api-keys-provider.tsx | Switches key-resolution calls to admin endpoints when adminMode is enabled. |
| web/src/features/keys/api.ts | Adds admin key-fetch APIs and batch APIs. |
| web/src/features/chat-logs/types.ts | Replaces legacy chat-log types with session/turn/transcript-oriented types. |
| web/src/features/chat-logs/lib/transcript.ts | Adds transcript builder for session turns. |
| web/src/features/chat-logs/lib/format-body.ts | Adds robust body formatter (JSON pretty print, SSE parsing/reassembly, truncated JSON reindent). |
| web/src/features/chat-logs/lib/tests/transcript.test.ts | Tests transcript building behavior. |
| web/src/features/chat-logs/lib/tests/format-body.test.ts | Tests body formatting, including Claude/OpenAI SSE reconstruction. |
| web/src/features/chat-logs/index.tsx | Switches page from logs table to sessions table. |
| web/src/features/chat-logs/components/session-detail-sheet.tsx | New transcript side-sheet rendering session meta and turn-by-turn content. |
| web/src/features/chat-logs/components/chat-sessions-table.tsx | Replaces logs table with sessions table + filters (token/user/model) and transcript opening. |
| web/src/features/chat-logs/components/chat-log-detail-sheet.tsx | Removes legacy per-log detail sheet. |
| web/src/features/chat-logs/api.ts | Updates frontend admin API calls to session-based endpoints + new query keys. |
| service/task_billing_test.go | Updates migrations/cleanup to new chat session/turn tables. |
| service/chat_log_session.go | Adds message-chain extraction + prefix-hash computation for session identification. |
| service/chat_log_session_test.go | Adds unit tests for chain extraction and hashing properties. |
| service/chat_log_persist.go | Replaces per-request ChatLog inserts with session resolution + turn inserts and session advancement. |
| service/chat_log_persist_test.go | Updates persistence tests to validate session chaining + deltas. |
| service/chat_log_capture.go | Simplifies response capture writer (now fully buffers response). |
| service/chat_log_capture_test.go | Updates capture writer tests to match new behavior. |
| service/channel_affinity_usage_cache_test.go | Makes test keys more collision-resistant across OS clock resolution. |
| router/chat-log-router.go | Switches admin chat-log routes to /sessions and /sessions/:id. |
| router/api-router.go | Adds admin token key retrieval endpoints (single + batch). |
| model/token.go | Allows GetTokenKeysByIds to bypass user filter when userId == 0 (admin batch use). |
| model/task_cas_test.go | Updates migrations/cleanup to new chat session/turn tables. |
| model/main.go | Migrates chatlog DB schema to sessions/turns and drops legacy chat_logs table. |
| model/chat_log.go | Replaces ChatLog model with ChatSession and ChatTurn models + queries. |
| model/chat_log_test.go | Updates model tests for sessions/turns behavior. |
| controller/token.go | Adds AdminGetTokenKey and AdminGetTokenKeysBatch handlers + audit event. |
| controller/token_test.go | Adds test coverage for admin cross-user token key retrieval. |
| controller/relay.go | Passes relayFormat into chat-log capture installer. |
| controller/chat_log.go | Replaces admin chat-log list/detail handlers with session/turn endpoints. |
| controller/chat_log_test.go | Updates controller tests to validate new session list/detail shapes. |
| controller/audit.go | Adds audit template for token.key_view. |
| .gitignore | Adds .superpowers/ ignore entry. |
Review details
Suppressed comments (3)
web/src/features/keys/api.ts:136
- Same indentation issue here: tabs and extra indentation diverge from the file’s formatting. Reformat to match the surrounding style.
web/src/features/keys/api.ts:150 - Same indentation issue here: tabs and extra indentation diverge from the file’s formatting. Reformat to match the surrounding style.
web/src/features/chat-logs/components/chat-sessions-table.tsx:11 - License header typo: missing “the” in “without even the implied warranty of”. Please keep the AGPL boilerplate consistent.
- Files reviewed: 41/42 changed files
- Comments generated: 10
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if err := CHATLOG_DB.AutoMigrate(&ChatSession{}, &ChatTurn{}); err != nil { | ||
| return err | ||
| } | ||
| if dbType == common.DatabaseTypeMySQL { | ||
| if err := CHATLOG_DB.Exec("ALTER TABLE chat_logs MODIFY COLUMN request_body LONGTEXT").Error; err != nil { | ||
| if err := CHATLOG_DB.Exec("ALTER TABLE chat_turns MODIFY COLUMN new_messages LONGTEXT").Error; err != nil { | ||
| return err | ||
| } | ||
| if err := CHATLOG_DB.Exec("ALTER TABLE chat_logs MODIFY COLUMN response_body LONGTEXT").Error; err != nil { | ||
| if err := CHATLOG_DB.Exec("ALTER TABLE chat_turns MODIFY COLUMN response_body LONGTEXT").Error; err != nil { | ||
| return err | ||
| } | ||
| if err := CHATLOG_DB.Exec("ALTER TABLE chat_sessions MODIFY COLUMN `system` LONGTEXT").Error; err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| return CHATLOG_DB.Exec("DROP TABLE IF EXISTS chat_logs").Error | ||
| } |
| // ponytail: response is buffered fully in memory per request; chat-log is | ||
| // opt-in per token, add a cap again if this ever becomes a memory problem. | ||
| func (w *chatLogCaptureWriter) Write(data []byte) (int, error) { | ||
| n, err := w.ResponseWriter.Write(data) | ||
| if err != nil { | ||
| return n, err | ||
| } | ||
| w.mu.Lock() | ||
| defer w.mu.Unlock() | ||
| if w.truncated { | ||
| return n, nil | ||
| } | ||
| remaining := w.maxBytes - w.buffer.Len() | ||
| if remaining <= 0 { | ||
| w.truncated = true | ||
| return n, nil | ||
| } | ||
| if len(data) <= remaining { | ||
| w.buffer.Write(data) | ||
| } else { | ||
| w.buffer.Write(data[:remaining]) | ||
| w.truncated = true | ||
| } | ||
| w.buffer.Write(data) | ||
| return n, nil |
| turn := &model.ChatTurn{ | ||
| SessionId: session.Id, TurnIndex: session.TurnCount, | ||
| RequestId: requestId, ModelName: modelName, ChannelId: channelId, | ||
| StatusCode: statusCode, UseTime: useTime, IsStream: isStream, | ||
| NewMessages: newMessages, ResponseBody: respBody, |
| func (r *ChatLogRecorder) SetRequestBody(body []byte) { | ||
| if r == nil || len(body) == 0 { | ||
| return | ||
| } | ||
| if len(body) > defaultChatLogMaxBodyBytes { | ||
| r.requestBody = string(body[:defaultChatLogMaxBodyBytes]) | ||
| return | ||
| } | ||
| r.requestBody = string(body) | ||
| } |
| This program is distributed in the hope that it will be useful, | ||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| but WITHOUT ANY WARRANTY; without even implied warranty of | ||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| You should have received a copy of the GNU Affero General License | ||
| along with this program. If not, see <https://www.gnu.org/licenses/>. |
| This program is distributed in the hope that it will be useful, | ||
| but WITHOUT ANY WARRANTY; without even implied warranty of | ||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| You should have received a copy of the GNU Affero General License | ||
| along with this program. If not, see <https://www.gnu.org/licenses/>. |
| This program is distributed in the hope that it will be useful, | ||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| but WITHOUT ANY WARRANTY; without even implied warranty of | ||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| // Batch fetch real (unmasked) keys for multiple tokens | ||
| export async function fetchTokenKeysBatch(ids: number[]): Promise<{ | ||
| success: boolean | ||
| message?: string | ||
| data?: { keys: Record<number, string> } | ||
| }> { | ||
| const res = await api.post('/api/token/batch/keys', { ids }) | ||
| return res.data | ||
| } | ||
|
|
||
| export async function fetchTokenKeysBatch(ids: number[]): Promise<{ | ||
| success: boolean | ||
| message?: string | ||
| data?: { |
从 fix-admin-copy-user-key 分支合入 develop。