diff --git a/.gitignore b/.gitignore index 1d8f78aa..be56aae0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ web/ deploy/docker-compose.override.yml .zcode/ +.learnings/ diff --git a/changelog/unreleased/qwenwork-provider.md b/changelog/unreleased/qwenwork-provider.md new file mode 100644 index 00000000..94e373b8 --- /dev/null +++ b/changelog/unreleased/qwenwork-provider.md @@ -0,0 +1,11 @@ +### English + +- Add the QwenWork provider (`qwenwork`), an in-process adapter for the QwenWork desktop product. Two regions under one provider: `global` (qwenwork.ai, default) and `cn` (千问办公, qwenwork.cn). Browser login uses the single-use device authorization link; poll and token refresh run in-process, with refresh-token rotation stored automatically. Chat goes through the COSY-signed `agent_chat_generation` endpoint and the wrapped upstream SSE is relayed as OpenAI chunks, so `/v1/chat/completions`, `/v1/messages`, and `/v1/responses` all work, streaming and non-streaming, with multi-turn history and tool calls. The model catalog (`qwork-advanced`, `qwork-auto`, …) is fetched live from the upstream `qwork` list, and quota comes from the account-context API. Model-level errors ("Model is not available for this user") cool only that model; quota code 14018 maps to account quota. Credentials can also be imported from a qwenwork2api account export (`accessToken`/`refreshToken`). The CN region follows the desktop bundle configuration but has not yet been verified with a real CN account; international accounts are verified end-to-end. +- Console: new QwenWork International / 千问办公 国内版 account types in the add-account wizard. The authorization link is single-use: open it directly in a browser already signed in to the target account, and do not send it through chat tools. Models are addressable with the `qwenwork/` prefix (for example `qwenwork/qwork-advanced`). +- Protocol behavior was cross-checked against nostalgia296/qwenwork2api-makers (no license declared); the Go implementation is independent and copies no code. + +### 中文 + +- 新增 QwenWork 供应商(`qwenwork`):QwenWork(千问办公)桌面产品的进程内适配器。同一供应商下两个 region:`global`(qwenwork.ai,默认)与 `cn`(qwenwork.cn)。浏览器登录使用一次性的设备授权链接;轮询与令牌刷新都在进程内完成,refresh_token 轮换后自动保存。对话走 COSY 签名的 `agent_chat_generation` 端点,上游包装 SSE 转换为 OpenAI 分块转发,因此 `/v1/chat/completions`、`/v1/messages`、`/v1/responses` 流式与非流式均可用,支持多轮历史与工具调用。模型目录(`qwork-advanced`、`qwork-auto` 等)实时取自上游 `qwork` 列表,额度来自 account-context 接口。模型级错误("Model is not available for this user")只冷却该模型;额度错误码 14018 映射为账号额度。也支持从 qwenwork2api 的账号导出(`accessToken`/`refreshToken`)导入凭证。国内版按桌面端配置实现,但尚未用真实国内版账号验收;国际版已端到端验证。 +- 控制台:添加账号向导新增 "QwenWork 国际版" 与 "千问办公 国内版" 两种类型。授权链接只能使用一次:请在已登录目标账号的浏览器窗口里直接打开,不要经聊天工具发送。模型可用 `qwenwork/` 前缀指定(例如 `qwenwork/qwork-advanced`)。 +- 协议行为参考并核对了 nostalgia296/qwenwork2api-makers(未声明许可证);Go 实现为独立编写,未复制其代码。 diff --git a/frontend/src/components/AddAccountModal.tsx b/frontend/src/components/AddAccountModal.tsx index 28825777..9505aa42 100644 --- a/frontend/src/components/AddAccountModal.tsx +++ b/frontend/src/components/AddAccountModal.tsx @@ -56,6 +56,8 @@ const labelKeys: Record = { 'command-global': 'accountTypeCommandGlobal', 'codex-global': 'accountTypeCodexGlobal', 'zhipu-cn': 'accountTypeZhipuCN', + 'qwenwork-global': 'accountTypeQwenworkGlobal', + 'qwenwork-cn': 'accountTypeQwenworkCN', } const hintKeys: Record = { @@ -68,6 +70,8 @@ const hintKeys: Record = { 'command-global': 'accountTypeCommandGlobalHint', 'codex-global': 'accountTypeCodexGlobalHint', 'zhipu-cn': 'accountTypeZhipuCNHint', + 'qwenwork-global': 'accountTypeQwenworkGlobalHint', + 'qwenwork-cn': 'accountTypeQwenworkCNHint', } function AccountTypeSkeleton({ ariaLabel }: { ariaLabel: string }) { @@ -439,6 +443,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { else if (activeOption.descriptor.id === 'trae') bundle.format = 'trae-oauth-v1' else if (activeOption.descriptor.id === 'devin') bundle.format = 'devin-session-v1' else if (activeOption.descriptor.id === 'command') bundle.format = 'command-key-v1' + else if (activeOption.descriptor.id === 'qwenwork') bundle.format = 'qwenwork-oauth-v1' else bundle.format = 'qoder-native-v1' } try { diff --git a/frontend/src/components/ProviderMark.tsx b/frontend/src/components/ProviderMark.tsx index 0025bca5..20892d3d 100644 --- a/frontend/src/components/ProviderMark.tsx +++ b/frontend/src/components/ProviderMark.tsx @@ -2,6 +2,7 @@ import { CommandMark } from '@/components/CommandMark' import { CodexMark } from '@/components/CodexMark' import { DevinMark } from '@/components/DevinMark' import { QoderMark } from '@/components/QoderMark' +import { QwenworkMark } from '@/components/QwenworkMark' import { TraeMark } from '@/components/TraeMark' import { WorkBuddyMark } from '@/components/WorkBuddyMark' import { ZhipuMark } from '@/components/ZhipuMark' @@ -33,5 +34,8 @@ export function ProviderMark({ provider, size = 16, className = '' }: Props) { if (id === 'zhipu') { return } + if (id === 'qwenwork') { + return + } return } diff --git a/frontend/src/components/QwenworkMark.tsx b/frontend/src/components/QwenworkMark.tsx new file mode 100644 index 00000000..4a0edc2e --- /dev/null +++ b/frontend/src/components/QwenworkMark.tsx @@ -0,0 +1,29 @@ +type Props = { + size?: number + className?: string +} + +// QwenWork mark: an ink tile, a Q glyph, and a Signal Cyan terminal dot — +// a simple geometric placeholder, deliberately not the official trademark. +export function QwenworkMark({ size = 16, className = '' }: Props) { + const dot = Math.max(2, Math.round(size * 0.14)) + return ( +