Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@

OpenGUI 会读取真实 Android App 界面,规划下一步操作,执行移动端动作,并返回结构化结果。

第一次使用 DSH 插件时,可先阅读 [OpenGUI × DeepSeek Harness 简明说明与 FAQ](./deepseek-harness-plugin/docs/quick-start-and-faq.zh.md)。

## 在 DeepSeek Harness 中使用 OpenGUI

macOS 上最短的路径,是让 Codex 运行 `main` 分支上的稳定安装 Skill。每次执行时,安装器都会解析并安装最新正式版 OpenGUI 插件,同时保留指定版本参数用于回滚。环境需要 Node.js 22.19+ 或 24+,兼容的 DSH 版本会自动安装。把下面整段作为一条消息发给 Codex:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions deepseek-harness-plugin/docs/install-for-beginners.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

这份指南面向不会编程的用户。你不需要下载源码、使用 Git、构建项目或修改 DeepSeek Harness。你只需要下载一个 `.tgz` 插件文件、复制几条命令,并填写模型服务信息。

如果你只想快速了解 DSH、完成首次使用或查询常见问题,请先看 [OpenGUI × DeepSeek Harness 简明说明与 FAQ](quick-start-and-faq.zh.md)。

## 目前能不能直接安装?

可以。从 [OpenGUI 的公开 Release](https://github.com/Core-Mate/OpenGUI/releases/tag/dsh-coremate-mobile-v0.1.13) 打开插件版本,在 Assets 中下载 `dsh-coremate-mobile-0.1.13.tgz`。
Expand Down
407 changes: 407 additions & 0 deletions deepseek-harness-plugin/docs/quick-start-and-faq.zh.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion deepseek-harness-plugin/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface PhoneModelConfiguration {
readonly baseURL?: string
readonly api: MobileApi
readonly model?: string
readonly models?: readonly { readonly id: string }[]
readonly apiKeyEnv: string
}

Expand Down Expand Up @@ -174,7 +175,7 @@ export async function configurePhoneModel(
force = false,
): Promise<PhoneConfigurationResult> {
const baseURL = force ? undefined : config.baseURL?.trim()
const model = force ? undefined : config.model?.trim()
const model = force ? undefined : config.models?.[0]?.id.trim() || config.model?.trim()
const ref = credentialRef(config.apiKeyEnv)
const storedKey = force ? undefined : await services.resolveCredential(ref)
if (baseURL && model && storedKey !== undefined) return { status: 'ready', changed: false }
Expand Down
61 changes: 50 additions & 11 deletions deepseek-harness-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ class PhonePiAiAdapter extends PiAiAdapter {
}
}

/** One model row written by the existing DSH provider editor. */
export interface ConfiguredModel {
id: string
name?: string
contextWindow?: number
maxTokens?: number
input?: ('text' | 'image')[]
}

/** User-owned phone model settings and local execution bounds. */
export interface Config {
/** OpenAI-compatible phone-model endpoint. */
Expand All @@ -103,6 +112,8 @@ export interface Config {
api?: MobileApi
/** Image- and tool-capable model identifier. */
model?: string
/** Model rows written by the existing DSH model editor. */
models?: ConfiguredModel[]
/** Credential reference used to resolve the API key. */
apiKeyEnv?: string
/** Prefer the receiving DSH model, or always use the dedicated fallback. */
Expand Down Expand Up @@ -145,10 +156,19 @@ const DEFAULT_CONFIG = {
} as const satisfies Required<Pick<Config,
'api' | 'apiKeyEnv' | 'modelStrategy' | 'trustUnknownCurrentModels' | 'trustedCurrentModels' | 'visionDeclarations' | 'commandTimeoutMs' | 'maxOperations' | 'maxParallelDevices' | 'contextWindow' | 'maxTokens' | 'streamIdleTimeoutMs'>>

const configuredModelSchema: z<ConfiguredModel> = z.object({
id: z.string().required(),
name: z.string(),
contextWindow: z.number().step(1).min(1),
maxTokens: z.number().step(1).min(1),
input: z.array(z.union(['text', 'image'] as const)),
})

export const Config: z<Config> = z.object({
baseURL: z.string(),
api: z.union(['openai-responses', 'openai-completions'] as const).default(DEFAULT_CONFIG.api),
model: z.string(),
models: z.array(configuredModelSchema),
apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_CONFIG.apiKeyEnv),
modelStrategy: z.union(['current-first', 'dedicated'] as const).default(DEFAULT_CONFIG.modelStrategy),
trustUnknownCurrentModels: z.boolean().default(DEFAULT_CONFIG.trustUnknownCurrentModels),
Expand Down Expand Up @@ -202,15 +222,33 @@ function resolvedConfig(config: Config): ResolvedConfig {
}
if (config.baseURL !== undefined) resolved.baseURL = config.baseURL
if (config.model !== undefined) resolved.model = config.model
if (config.models !== undefined) resolved.models = config.models.map(model => ({ ...model }))
if (config.adbPath !== undefined) resolved.adbPath = config.adbPath
return resolved
}

/** Resolve the editor's first model row while preserving legacy single-model settings. */
export function configuredModel(config: Config): {
id: string
contextWindow: number
maxTokens: number
} | undefined {
const value = resolvedConfig(config)
const edited = value.models?.[0]
const id = edited?.id.trim() || value.model?.trim()
if (!id) return undefined
return {
id,
contextWindow: edited?.contextWindow ?? value.contextWindow,
maxTokens: edited?.maxTokens ?? value.maxTokens,
}
}

function configuredProfile(config: Config): ResolvedPiAiProviderProfile | undefined {
const value = resolvedConfig(config)
const baseURL = value.baseURL?.trim()
const model = value.model?.trim()
if (!baseURL || !model) return undefined
const selected = configuredModel(value)
if (!baseURL || selected === undefined) return undefined
const url = new URL(baseURL)
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
throw new Error('coremate-mobile: the phone model endpoint must use HTTP or HTTPS')
Expand All @@ -220,10 +258,10 @@ function configuredProfile(config: Config): ResolvedPiAiProviderProfile | undefi
displayName: 'OpenGUI model',
baseURL,
api: value.api,
model,
model: selected.id,
apiKeyEnv: value.apiKeyEnv,
contextWindow: value.contextWindow,
maxTokens: value.maxTokens,
contextWindow: selected.contextWindow,
maxTokens: selected.maxTokens,
streamIdleTimeoutMs: value.streamIdleTimeoutMs,
})
}
Expand Down Expand Up @@ -610,7 +648,7 @@ export function apply(ctx: Context, baseConfig: Config): void {
registeredModel = undefined
return
}
const model = resolvedConfig(current()).model
const model = configuredModel(current())?.id
if (registration === undefined) registration = ctx.llm.registerAdapter([PROVIDER], adapter)
else if (registeredModel !== model) registration.replace([PROVIDER])
registeredModel = model
Expand All @@ -629,11 +667,12 @@ export function apply(ctx: Context, baseConfig: Config): void {
const credentials = ctx.get('credentials')
const initial = resolvedConfig(current())
const initialProfile = profile()
const initialModel = configuredModel(initial)
const initialKey = credentials === undefined
? undefined
: await credentials.resolve(credentialRef(initial.apiKeyEnv))
if (!force && initialProfile !== undefined && initial.model?.trim() && initialKey !== undefined) {
return { provider: PROVIDER, model: initial.model.trim(), maxTokens: initial.maxTokens }
if (!force && initialProfile !== undefined && initialModel !== undefined && initialKey !== undefined) {
return { provider: PROVIDER, model: initialModel.id, maxTokens: initialModel.maxTokens }
}
if (questions === undefined || credentials === undefined) {
throw new Error('coremate-mobile: 当前 Host 不支持对话式配置;请在 settings.yaml 和凭据存储中配置 OpenGUI 模型')
Expand All @@ -650,11 +689,11 @@ export function apply(ctx: Context, baseConfig: Config): void {
const value = resolvedConfig(current())
const active = profile()
const key = await credentials.resolve(credentialRef(value.apiKeyEnv))
const model = value.model?.trim()
if (active === undefined || !model || key === undefined) {
const selected = configuredModel(value)
if (active === undefined || selected === undefined || key === undefined) {
throw new Error('coremate-mobile: 专用视觉模型配置未完整保存')
}
return { provider: PROVIDER, model, maxTokens: value.maxTokens }
return { provider: PROVIDER, model: selected.id, maxTokens: selected.maxTokens }
}

const inheritedOptions = (options: AgentOptions): AgentOptions => inheritedAgentOptions(options)
Expand Down
15 changes: 15 additions & 0 deletions deepseek-harness-plugin/tests/configuration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ describe('OpenGUI interactive model configuration', () => {
expect(io.updateSettings).not.toHaveBeenCalled()
})

it('reuses the model directory entry when only the credential is missing', async () => {
const io = services({ apiKey: 'sk-phone', capabilityConfirmation: '确认支持并保存' })
const config = {
...emptyConfig(),
baseURL: 'https://gateway.example/v1',
models: [{ id: 'editor-model' }],
}

await expect(configurePhoneModel(config, io, invocation())).resolves.toEqual({ status: 'ready', changed: true })
expect(io.ask.mock.calls.map(call => (call[0] as AskUserQuestionRequest).questions[0]?.id))
.toEqual(['apiKey', 'capabilityConfirmation'])
expect(io.storeCredential).toHaveBeenCalled()
expect(io.updateSettings).not.toHaveBeenCalled()
})

it('preserves an existing endpoint and protocol while asking only for missing model and key', async () => {
const io = services({ model: 'vision-model', apiKey: 'sk-phone', capabilityConfirmation: '确认支持并保存' })
const config = { ...emptyConfig(), baseURL: 'https://gateway.example/v1', api: 'openai-completions' as const }
Expand Down
47 changes: 47 additions & 0 deletions deepseek-harness-plugin/tests/model-editor-config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import { Config, configuredModel } from '../src/index.ts'

describe('OpenGUI model editor configuration', () => {
it('keeps the legacy single-model fields working', () => {
expect(configuredModel({
model: ' legacy-vision ',
contextWindow: 131_072,
maxTokens: 16_384,
})).toEqual({
id: 'legacy-vision',
contextWindow: 131_072,
maxTokens: 16_384,
})
})

it('uses the first model saved by the existing DSH model editor', () => {
const parsed = Config({
contextWindow: 131_072,
maxTokens: 16_384,
models: [{
id: ' edited-vision ',
name: 'Edited Vision',
contextWindow: 262_144,
maxTokens: 32_768,
input: ['text', 'image'],
}],
})

expect(configuredModel(parsed)).toEqual({
id: 'edited-vision',
contextWindow: 262_144,
maxTokens: 32_768,
})
})

it('falls back per capacity when the editor leaves a value blank', () => {
expect(configuredModel({
model: 'legacy-vision',
contextWindow: 131_072,
maxTokens: 16_384,
models: [{ id: 'edited-vision' }],
})).toEqual({
id: 'edited-vision', contextWindow: 131_072, maxTokens: 16_384,
})
})
})
Loading