Repository navigation
feat(weixin): add image sending support via CDN upload #3781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
9149336
f85bbab
0a25548
dbf3da8
17600d5
cbcc0f7
9c3964c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,7 +17,7 @@ | |||||||||||||||||||||||||||||||||||||||||
| import { loadAccount, DEFAULT_BASE_URL } from './accounts.js'; | ||||||||||||||||||||||||||||||||||||||||||
| import { startPollLoop, getContextToken } from './monitor.js'; | ||||||||||||||||||||||||||||||||||||||||||
| import type { CdnRef, FileCdnRef } from './monitor.js'; | ||||||||||||||||||||||||||||||||||||||||||
| import { sendText } from './send.js'; | ||||||||||||||||||||||||||||||||||||||||||
| import { sendText, sendImage } from './send.js'; | ||||||||||||||||||||||||||||||||||||||||||
| import { downloadAndDecrypt } from './media.js'; | ||||||||||||||||||||||||||||||||||||||||||
| import { getConfig, sendTyping } from './api.js'; | ||||||||||||||||||||||||||||||||||||||||||
| import { TypingStatus } from './types.js'; | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -43,6 +43,22 @@ | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| async connect(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||
| // Default channel instructions: tell the AI it can send images via WeChat | ||||||||||||||||||||||||||||||||||||||||||
| if (!this.config.instructions) { | ||||||||||||||||||||||||||||||||||||||||||
| this.config.instructions = [ | ||||||||||||||||||||||||||||||||||||||||||
| '## WeChat Channel Capabilities', | ||||||||||||||||||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||||||||||||||||||
| 'You are communicating with users through WeChat. You CAN send images to the user.', | ||||||||||||||||||||||||||||||||||||||||||
| 'To send an image, include it in your response using this EXACT format:', | ||||||||||||||||||||||||||||||||||||||||||
| '[IMAGE: 图片文件的完整绝对路径]', | ||||||||||||||||||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||||||||||||||||||
| 'Example: 如果你想发送 /tmp/cat.png 给用户, 在回复中写 [IMAGE: /tmp/cat.png]', | ||||||||||||||||||||||||||||||||||||||||||
| 'This marker will be automatically removed from the text and the image will be uploaded and sent.', | ||||||||||||||||||||||||||||||||||||||||||
| 'You can include multiple [IMAGE: ...] markers in one response.', | ||||||||||||||||||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||||||||||||||||||
| 'Users can also send you images, which you can see and analyze.', | ||||||||||||||||||||||||||||||||||||||||||
| ].join('\n'); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| const account = loadAccount(); | ||||||||||||||||||||||||||||||||||||||||||
| if (!account) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -153,18 +169,72 @@ | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Always remind the AI about image-sending capability on every message | ||||||||||||||||||||||||||||||||||||||||||
| const IMAGE_INSTRUCTION = | ||||||||||||||||||||||||||||||||||||||||||
| '[WeChat Channel] 你可以通过微信发送图片。在回复中使用 [IMAGE: 文件绝对路径] 发送图片,例如 [IMAGE: /tmp/cat.png]。标记会被自动移除。'; | ||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Per-message IMAGE_INSTRUCTION is redundant and increases attack surface
Consider removing the per-message injection and relying solely on — pai/glm-5 via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||
| envelope.text = `${IMAGE_INSTRUCTION}\n\n${envelope.text}`; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| await super.handleInbound(envelope); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| async sendMessage(chatId: string, text: string): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||
| async sendMessage( | ||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The base class — pai/glm-5 via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||
| chatId: string, | ||||||||||||||||||||||||||||||||||||||||||
| text: string, | ||||||||||||||||||||||||||||||||||||||||||
| imagePaths?: string[], | ||||||||||||||||||||||||||||||||||||||||||
| ): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||
| const contextToken = getContextToken(chatId) || ''; | ||||||||||||||||||||||||||||||||||||||||||
| await sendText({ | ||||||||||||||||||||||||||||||||||||||||||
| to: chatId, | ||||||||||||||||||||||||||||||||||||||||||
| text, | ||||||||||||||||||||||||||||||||||||||||||
| baseUrl: this.baseUrl, | ||||||||||||||||||||||||||||||||||||||||||
| token: this.token, | ||||||||||||||||||||||||||||||||||||||||||
| contextToken, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Parse [IMAGE: /path/to/file.png] markers from text | ||||||||||||||||||||||||||||||||||||||||||
| const imageRegex = /\[IMAGE:\s*([^\]]+)\]/gi; | ||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] [IMAGE:] regex matches inside code blocks and produces empty paths Two issues with the regex
For (2), filter empty paths after trimming:
Suggested change
For (1), consider stripping code blocks before running the regex, or extracting the parsing into a testable pure function. — pai/glm-5 via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||
| const parsedImages: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||
| let cleanedText = text.replace(imageRegex, (_, path: string) => { | ||||||||||||||||||||||||||||||||||||||||||
| parsedImages.push(path.trim()); | ||||||||||||||||||||||||||||||||||||||||||
| return ''; | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
Check failureCode scanning / CodeQL Polynomial regular expression used on uncontrolled data High
This
regular expression Error loading related location Loading library input Error loading related location Loading |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Merge with any imagePaths from the ACP pipeline | ||||||||||||||||||||||||||||||||||||||||||
| const allImages = [...(imagePaths || []), ...parsedImages]; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Clean up double blank lines left by removed markers | ||||||||||||||||||||||||||||||||||||||||||
| cleanedText = cleanedText.replace(/\n{3,}/g, '\n\n').trim(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Send text first if non-empty | ||||||||||||||||||||||||||||||||||||||||||
| if (cleanedText) { | ||||||||||||||||||||||||||||||||||||||||||
| await sendText({ | ||||||||||||||||||||||||||||||||||||||||||
| to: chatId, | ||||||||||||||||||||||||||||||||||||||||||
| text: cleanedText, | ||||||||||||||||||||||||||||||||||||||||||
| baseUrl: this.baseUrl, | ||||||||||||||||||||||||||||||||||||||||||
| token: this.token, | ||||||||||||||||||||||||||||||||||||||||||
| contextToken, | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Send images | ||||||||||||||||||||||||||||||||||||||||||
| if (allImages.length) { | ||||||||||||||||||||||||||||||||||||||||||
| for (const imagePath of allImages) { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| await sendImage({ | ||||||||||||||||||||||||||||||||||||||||||
| to: chatId, | ||||||||||||||||||||||||||||||||||||||||||
| imagePath, | ||||||||||||||||||||||||||||||||||||||||||
| baseUrl: this.baseUrl, | ||||||||||||||||||||||||||||||||||||||||||
| token: this.token, | ||||||||||||||||||||||||||||||||||||||||||
| contextToken, | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||
| const errMsg = err instanceof Error ? err.message : String(err); | ||||||||||||||||||||||||||||||||||||||||||
| process.stderr.write( | ||||||||||||||||||||||||||||||||||||||||||
| `[Weixin:${this.name}] Failed to send image ${imagePath}: ${errMsg}\n`, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| await sendText({ | ||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] Fallback sendText not wrapped in try/catch — unhandled rejection When Also,
Suggested change
— pai/glm-5 via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||
| to: chatId, | ||||||||||||||||||||||||||||||||||||||||||
| text: `图片发送失败: ${errMsg}`, | ||||||||||||||||||||||||||||||||||||||||||
| baseUrl: this.baseUrl, | ||||||||||||||||||||||||||||||||||||||||||
| token: this.token, | ||||||||||||||||||||||||||||||||||||||||||
| contextToken, | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| disconnect(): void { | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -141,3 +141,101 @@ export async function sendTyping( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body: SendTypingReq = { ...req, base_info: baseInfo() }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return post<SendTypingResp>(baseUrl, '/ilink/bot/sendtyping', body, token); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface GetUploadUrlReq { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filekey: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| media_type: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to_user_id: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawsize: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawfilemd5: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filesize: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| no_need_thumb: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aeskey: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| base_info: BaseInfo; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface GetUploadUrlResp { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ret?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errmsg?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upload_full_url?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upload_param?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thumb_upload_param?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Request an upload URL and CDN credentials for media. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param aeskeyHex 16-byte AES key as 32-char hex string (e.g. "00112233445566778899aabbccddeeff") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns Either the full CDN upload URL or the upload_param string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getUploadUrl( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| baseUrl: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| token: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toUserId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filekey: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawsize: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawfilemd5: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| encryptedSize: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aeskeyHex: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body: GetUploadUrlReq = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filekey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| media_type: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to_user_id: toUserId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawsize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawfilemd5, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] 错误消息中包含字面量
Suggested change
— deepseek-v4-pro via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filesize: encryptedSize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| no_need_thumb: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aeskey: aeskeyHex, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical]
Suggested change
— deepseek-v4-pro via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| base_info: baseInfo(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resp = await post<GetUploadUrlResp>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| baseUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '/ilink/bot/getuploadurl', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| token, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] getUploadUrl doesn't verify ret === 0 The function checks for
Suggested change
Place this check before the — pai/glm-5 via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // upload_full_url: CDN upload URL with all params embedded | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (resp.upload_full_url) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resp.upload_full_url; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // upload_param: CDN upload params only (must construct URL with filekey) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (resp.upload_param) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resp.upload_param; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `getuploadurl failed: ret=${resp.ret} errmsg=${resp.errmsg || '(none)'}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Upload encrypted media to CDN. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * If urlOrParam is a full URL, use it directly. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * If it's just a param, construct the URL. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function uploadToCdn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| urlOrParam: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filekey: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] SSRF — no URL host validation
Suggested change
— pai/glm-5 via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| encryptedData: Buffer, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = urlOrParam.startsWith('http') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? urlOrParam | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : `https://novac2c.cdn.weixin.qq.com/c2c/upload?encrypted_query_param=${encodeURIComponent(urlOrParam)}&filekey=${encodeURIComponent(filekey)}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resp = await fetch(url, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { 'Content-Type': 'application/octet-stream' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] CDN upload has no timeout — can hang indefinitely
Suggested change
— pai/glm-5 via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: encryptedData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!resp.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`CDN upload failed: HTTP ${resp.status}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract x-encrypted-param from response header | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const encryptParam = resp.headers.get('x-encrypted-param'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!encryptParam) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'CDN upload succeeded but missing x-encrypted-param header', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return encryptParam; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] Default instructions only set when falsy — custom instructions lose image capability
if (!this.config.instructions)means any channel with custom instructions silently loses the image-sending guide. The[IMAGE: ...]parsing still works unconditionally, so the mismatch creates inconsistent behavior.— pai/glm-5 via Qwen Code /review