feat: add searxng web-search provider, openrouter cost balance UI, offline scripts; update source-launch and docs
Some checks failed
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / windows node 24 / wine blocking (push) Has been skipped
CI / wine apt cache (push) Successful in 58s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 2m46s
Deploy documentation / deploy (push) Has been skipped
E2E (real DeepSeek API) / e2e (push) Failing after 1m18s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m18s
Landlock Run / Matrix (push) Successful in 13s
Release (vendor) / Pack npm tarballs (push) Failing after 3m43s
Release (dsh) / Pack npm tarballs (push) Failing after 1m53s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m51s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
Some checks failed
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / windows node 24 / wine blocking (push) Has been skipped
CI / wine apt cache (push) Successful in 58s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 2m46s
Deploy documentation / deploy (push) Has been skipped
E2E (real DeepSeek API) / e2e (push) Failing after 1m18s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m18s
Landlock Run / Matrix (push) Successful in 13s
Release (vendor) / Pack npm tarballs (push) Failing after 3m43s
Release (dsh) / Pack npm tarballs (push) Failing after 1m53s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m51s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// @vitest-environment jsdom
|
||||
// BalanceBadge presentation: polls an injected snapshot callback and renders
|
||||
// 余额 in both widths, hiding unknown figures behind the empty placeholder.
|
||||
|
||||
import { act, cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type { OpenRouterBalance } from '@deepseek-ai/dsh-openrouter-usage/client'
|
||||
import { BALANCE_POLL_MS, BalanceBadge, type BalanceBadgeProps } from '../src/client/BalanceBadge.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
const t = makeTranslate(zh, commonZh)
|
||||
|
||||
function makeBalance(over: Partial<OpenRouterBalance> = {}): OpenRouterBalance {
|
||||
return {
|
||||
balanceUsd: 10.5,
|
||||
label: 'prod-key',
|
||||
usageTokens: 1000,
|
||||
limitTokens: 100000,
|
||||
isFreeTier: false,
|
||||
updatedAt: Date.now(),
|
||||
currency: 'USD',
|
||||
...over,
|
||||
}
|
||||
}
|
||||
|
||||
function makeProps(over: Partial<BalanceBadgeProps> = {}) {
|
||||
return {
|
||||
wide: true,
|
||||
snapshot: vi.fn(async () => makeBalance()),
|
||||
t,
|
||||
...over,
|
||||
} as unknown as Parameters<typeof BalanceBadge>[0]
|
||||
}
|
||||
|
||||
describe('BalanceBadge', () => {
|
||||
it('renders the balance label with the USD figure in wide mode', async () => {
|
||||
render(<BalanceBadge {...makeProps()} />)
|
||||
expect(await screen.findByLabelText('余额 $10.50')).toBeTruthy()
|
||||
expect(screen.getByText('余额 $10.50')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('polls the snapshot callback on the balance interval', async () => {
|
||||
vi.useFakeTimers()
|
||||
const snapshot = vi.fn(async () => makeBalance())
|
||||
render(<BalanceBadge {...makeProps({ snapshot })} />)
|
||||
await act(async () => { await Promise.resolve() })
|
||||
expect(snapshot).toHaveBeenCalledTimes(1)
|
||||
await act(async () => { vi.advanceTimersByTime(BALANCE_POLL_MS) })
|
||||
await act(async () => { await Promise.resolve() })
|
||||
expect(snapshot).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('renders only the rail marker in the collapsed sidebar', async () => {
|
||||
render(<BalanceBadge {...makeProps({ wide: false })} />)
|
||||
expect(await screen.findByLabelText('余额 $10.50')).toBeTruthy()
|
||||
expect(screen.queryByText('余额 $10.50')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows the empty placeholder when no balance is known yet', async () => {
|
||||
render(<BalanceBadge {...makeProps({ snapshot: vi.fn(async () => makeBalance({ balanceUsd: null })) })} />)
|
||||
expect(await screen.findByLabelText('余额 ——')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,134 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* ui-openrouter-usage browser half on a real cordis Context with fake slots/
|
||||
* locale/remote faces: the plugin registers the CostDock entry at
|
||||
* conversation.composer.dock and the BalanceBadge entry at
|
||||
* sidebar.footer.action, the balance snapshot reads roll through the injected
|
||||
* face at call time, and registration disposal rides the plugin fiber (HMR
|
||||
* safety). The node half and the invariant companion are exercised over the
|
||||
* same Context.
|
||||
*/
|
||||
import { Context, Service } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import { afterEach } from 'vitest'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { OpenRouterBalance } from '@deepseek-ai/dsh-openrouter-usage/client'
|
||||
import type { BalanceBadgeActions } from '../src/client/BalanceBadge.tsx'
|
||||
import { BalanceBadge } from '../src/client/BalanceBadge.tsx'
|
||||
import { CostDock } from '../src/client/CostDock.tsx'
|
||||
import { apply, inject, NS } from '../src/client/index.ts'
|
||||
import { apply as nodeApply } from '../src/index.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
async function bench() {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SlotRegistry).await()
|
||||
const locale = new LocaleRuntime(ctx)
|
||||
ctx.provide('locale', locale)
|
||||
class RemoteService extends Service {
|
||||
constructor(serviceCtx: Context) {
|
||||
super(serviceCtx, 'remote')
|
||||
}
|
||||
}
|
||||
new RemoteService(ctx)
|
||||
const snapshot = vi.fn<() => Promise<RemoteResult<OpenRouterBalance>>>()
|
||||
.mockResolvedValue({ ok: true, value: balance() })
|
||||
ctx.provide('remote.openRouterUsage', { snapshot })
|
||||
return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, snapshot }
|
||||
}
|
||||
|
||||
function declare(slots: SlotRegistry): () => void {
|
||||
return slots.register({
|
||||
name: 'root',
|
||||
children: {
|
||||
'conversation.composer.dock': { kind: 'list', scope: 'session' },
|
||||
'sidebar.footer.action': { kind: 'list', scope: 'root' },
|
||||
},
|
||||
} as never, () => null)
|
||||
}
|
||||
|
||||
function balance(over: Partial<OpenRouterBalance> = {}): OpenRouterBalance {
|
||||
return {
|
||||
balanceUsd: 5.5,
|
||||
label: 'k',
|
||||
usageTokens: 0,
|
||||
limitTokens: 0,
|
||||
isFreeTier: false,
|
||||
updatedAt: 1_700_000_000_000,
|
||||
currency: 'USD',
|
||||
...over,
|
||||
}
|
||||
}
|
||||
|
||||
describe('ui-openrouter-usage browser plugin', () => {
|
||||
it('declares only the services its two entries read', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.openRouterUsage'])
|
||||
})
|
||||
|
||||
it('registers localized cost and balance entries without reading the Remote eagerly', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
|
||||
const cost = b.slots.entries('conversation.composer.dock')[0]!
|
||||
expect(cost.component).toBe(CostDock)
|
||||
expect(cost.options).toMatchObject({ id: 'openrouter-cost', order: 5 })
|
||||
expect(cost.locale).toBe(NS)
|
||||
|
||||
const badge = b.slots.entries('sidebar.footer.action')[0]!
|
||||
expect(badge.component).toBe(BalanceBadge)
|
||||
expect(badge.options).toMatchObject({ id: 'openrouter-balance', order: 10 })
|
||||
expect(badge.locale).toBe(NS)
|
||||
|
||||
// Registration reads nothing live; the snapshot rolls at call time.
|
||||
expect(b.snapshot).not.toHaveBeenCalled()
|
||||
const injected = (badge.inject as unknown as () => BalanceBadgeActions)()
|
||||
await expect(injected.snapshot()).resolves.toEqual(balance())
|
||||
expect(b.snapshot).toHaveBeenCalledOnce()
|
||||
await b.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('forwards a Remote failure out of the injected snapshot verbatim', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
b.snapshot.mockResolvedValueOnce({ ok: false, error: { code: 'REMOTE_ERROR', message: 'boom', details: {} } })
|
||||
const injected = (b.slots.entries('sidebar.footer.action')[0]!.inject as unknown as () => BalanceBadgeActions)()
|
||||
await expect(injected.snapshot()).rejects.toThrow('openRouterUsage.snapshot failed: REMOTE_ERROR: boom')
|
||||
await b.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('follows locale and recovers across late declaration and declarer reload', async () => {
|
||||
const b = await bench()
|
||||
const fiber = b.ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0)
|
||||
expect(b.slots.entries('sidebar.footer.action')).toHaveLength(0)
|
||||
|
||||
const stop = declare(b.slots)
|
||||
await vi.waitFor(() => { expect(b.slots.entries('conversation.composer.dock')).toHaveLength(1) })
|
||||
expect(b.slots.entries('sidebar.footer.action')).toHaveLength(1)
|
||||
|
||||
stop()
|
||||
expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0)
|
||||
declare(b.slots)
|
||||
await vi.waitFor(() => {
|
||||
expect(b.slots.entries('sidebar.footer.action')[0]?.component).toBe(BalanceBadge)
|
||||
})
|
||||
|
||||
await fiber.dispose()
|
||||
expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0)
|
||||
expect(b.slots.entries('sidebar.footer.action')).toHaveLength(0)
|
||||
await b.ctx.fiber.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ui-openrouter-usage node half', () => {
|
||||
it('the node apply is an inert loader seat', () => {
|
||||
expect(() => { nodeApply() }).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
// @vitest-environment jsdom
|
||||
// CostDock presentation: renders the running session cost from the
|
||||
// openRouterCost projection, and nothing at all until a step was priced.
|
||||
|
||||
import { act, cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { CostDock } from '../src/client/CostDock.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
const t = makeTranslate(zh, commonZh)
|
||||
|
||||
function makeProps(over: Partial<Parameters<typeof CostDock>[0]> = {}) {
|
||||
return {
|
||||
useProjection: vi.fn(() => undefined),
|
||||
t,
|
||||
...over,
|
||||
} as unknown as Parameters<typeof CostDock>[0]
|
||||
}
|
||||
|
||||
describe('CostDock', () => {
|
||||
it('renders nothing while the projection is absent or priced zero steps', () => {
|
||||
for (const value of [undefined, { totalUsd: 0, pricedSteps: 0, unknownModelSteps: 0, currency: 'USD' }]) {
|
||||
const { container } = render(<CostDock {...makeProps({ useProjection: () => value })} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('renders the priced session cost as a USD figure', () => {
|
||||
render(<CostDock {...makeProps({
|
||||
useProjection: () => ({ totalUsd: 1.2345, pricedSteps: 2, unknownModelSteps: 0, currency: 'USD' }),
|
||||
})} />)
|
||||
expect(screen.getByText('会话费用 $1.23')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('surfaces the unknown-pricing step count in a tooltip when present', () => {
|
||||
render(<CostDock {...makeProps({
|
||||
useProjection: () => ({ totalUsd: 0.5, pricedSteps: 1, unknownModelSteps: 3, currency: 'USD' }),
|
||||
})} />)
|
||||
expect(screen.getByText('会话费用 $0.50').getAttribute('title')).toBe('包含 3 个未知定价步骤')
|
||||
})
|
||||
|
||||
it('omits the tooltip when every step priced', () => {
|
||||
render(<CostDock {...makeProps({
|
||||
useProjection: () => ({ totalUsd: 0.5, pricedSteps: 1, unknownModelSteps: 0, currency: 'USD' }),
|
||||
})} />)
|
||||
expect(screen.getByText('会话费用 $0.50').getAttribute('title')).toBeNull()
|
||||
})
|
||||
|
||||
it('reads the projection through the openRouterCost key', () => {
|
||||
const useProjection = vi.fn(() => undefined)
|
||||
render(<CostDock {...makeProps({ useProjection })} />)
|
||||
act(() => {})
|
||||
expect(useProjection).toHaveBeenCalledWith('openRouterCost')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user