fix: job edit schedule format error and refactor services directory
- Fix #25: job update sends schedule as plain string but upstream expects { kind, expr, display } object, causing "'str' object has no attribute 'get'" - Move hermes-cli.ts, hermes.ts, hermes-profile.ts into services/hermes/ for multi-agent namespacing consistency - Fix ts-node Set spread compatibility in filesystem.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,7 @@ export interface CreateJobRequest {
|
||||
|
||||
export interface UpdateJobRequest {
|
||||
name?: string
|
||||
schedule?: string
|
||||
schedule?: string | { kind: string; expr: string; display: string }
|
||||
prompt?: string
|
||||
deliver?: string
|
||||
skills?: string[]
|
||||
|
||||
@@ -48,6 +48,8 @@ const targetOptions = computed(() => [
|
||||
{ label: t('jobs.local'), value: 'local' },
|
||||
])
|
||||
|
||||
const originalSchedule = ref<{ kind: string; expr: string; display: string } | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.jobId) {
|
||||
try {
|
||||
@@ -60,6 +62,9 @@ onMounted(async () => {
|
||||
deliver: job.deliver || 'origin',
|
||||
repeat_times: typeof job.repeat === 'number' ? job.repeat : (typeof job.repeat === 'object' ? job.repeat.times : null),
|
||||
}
|
||||
if (typeof job.schedule === 'object' && job.schedule) {
|
||||
originalSchedule.value = job.schedule
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(t('jobs.loadFailed') + ': ' + e.message)
|
||||
}
|
||||
@@ -86,6 +91,14 @@ async function handleSave() {
|
||||
repeat: formData.value.repeat_times ?? undefined,
|
||||
}
|
||||
|
||||
if (isEdit.value && originalSchedule.value) {
|
||||
(payload as any).schedule = {
|
||||
kind: originalSchedule.value.kind,
|
||||
expr: formData.value.schedule,
|
||||
display: formData.value.schedule,
|
||||
}
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await jobsStore.updateJob(props.jobId!, payload)
|
||||
message.success(t('jobs.jobUpdated'))
|
||||
|
||||
@@ -10,7 +10,7 @@ import { config } from './config'
|
||||
import { hermesRoutes, setupTerminalWebSocket, proxyMiddleware } from './routes/hermes'
|
||||
import { uploadRoutes } from './routes/upload'
|
||||
import { webhookRoutes } from './routes/webhook'
|
||||
import * as hermesCli from './services/hermes-cli'
|
||||
import * as hermesCli from './services/hermes/hermes-cli'
|
||||
import { getToken, authMiddleware } from './services/auth'
|
||||
|
||||
function getLocalVersion(): string {
|
||||
@@ -232,7 +232,7 @@ function bindShutdown() {
|
||||
async function ensureApiServerConfig() {
|
||||
const { readFileSync, writeFileSync, existsSync, copyFileSync } = await import('fs')
|
||||
const yaml = (await import('js-yaml')).default
|
||||
const { getActiveConfigPath } = await import('./services/hermes-profile')
|
||||
const { getActiveConfigPath } = await import('./services/hermes/hermes-profile')
|
||||
const configPath = getActiveConfigPath()
|
||||
|
||||
const defaults: Record<string, any> = {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { readFile, writeFile, copyFile } from 'fs/promises'
|
||||
import { chmod } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import YAML from 'js-yaml'
|
||||
import { restartGateway } from '../../services/hermes-cli'
|
||||
import { getActiveConfigPath, getActiveEnvPath, getActiveProfileDir } from '../../services/hermes-profile'
|
||||
import { restartGateway } from '../../services/hermes/hermes-cli'
|
||||
import { getActiveConfigPath, getActiveEnvPath, getActiveProfileDir } from '../../services/hermes/hermes-profile'
|
||||
|
||||
// Platform sections that require gateway restart after config change
|
||||
const PLATFORM_SECTIONS = new Set([
|
||||
|
||||
@@ -2,8 +2,8 @@ import Router from '@koa/router'
|
||||
import { readdir, readFile, stat, writeFile, mkdir, copyFile } from 'fs/promises'
|
||||
import { join, resolve } from 'path'
|
||||
import YAML from 'js-yaml'
|
||||
import { getActiveProfileDir, getActiveConfigPath, getActiveAuthPath, getActiveEnvPath } from '../../services/hermes-profile'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
import { getActiveProfileDir, getActiveConfigPath, getActiveAuthPath, getActiveEnvPath } from '../../services/hermes/hermes-profile'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
|
||||
// --- Provider env var mapping (from hermes providers.py HERMES_OVERLAYS + config.py) ---
|
||||
// Maps provider key → { api_key_envs: all env var aliases for API key, base_url_env: env var for base URL }
|
||||
@@ -513,7 +513,7 @@ fsRoutes.get('/api/hermes/available-models', async (ctx) => {
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled' && result.value.models.length > 0) {
|
||||
const { key, label, base_url, models } = result.value
|
||||
groups.push({ provider: key, label, base_url, models: [...new Set(models)] })
|
||||
groups.push({ provider: key, label, base_url, models: Array.from(new Set(models)) })
|
||||
} else if (result.status === 'rejected') {
|
||||
console.error(`[available-models] Failed: ${result.reason?.message || result.reason}`)
|
||||
}
|
||||
@@ -524,7 +524,7 @@ fsRoutes.get('/api/hermes/available-models', async (ctx) => {
|
||||
const dedupedGroups: typeof groups = []
|
||||
const seenProviders = new Map<string, number>()
|
||||
for (const g of groups) {
|
||||
g.models = [...new Set(g.models)]
|
||||
g.models = Array.from(new Set(g.models))
|
||||
const existingIdx = seenProviders.get(g.provider)
|
||||
if (existingIdx !== undefined) {
|
||||
// Merge models into existing group
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Router from '@koa/router'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
|
||||
export const logRoutes = new Router()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mkdir, writeFile } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import { tmpdir, homedir } from 'os'
|
||||
import YAML from 'js-yaml'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
|
||||
const apiServerDefaults = {
|
||||
enabled: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Router from '@koa/router'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
|
||||
export const sessionRoutes = new Router()
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import axios from 'axios'
|
||||
import { readFile, writeFile } from 'fs/promises'
|
||||
import { chmod } from 'fs/promises'
|
||||
import { resolve } from 'path'
|
||||
import { restartGateway } from '../../services/hermes-cli'
|
||||
import { getActiveEnvPath } from '../../services/hermes-profile'
|
||||
import { restartGateway } from '../../services/hermes/hermes-cli'
|
||||
import { getActiveEnvPath } from '../../services/hermes/hermes-profile'
|
||||
|
||||
const envPath = () => getActiveEnvPath()
|
||||
const ILINK_BASE = 'https://ilinkai.weixin.qq.com'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Router from '@koa/router'
|
||||
import { emitWebhook } from '../services/hermes'
|
||||
import { emitWebhook } from '../services/hermes/hermes'
|
||||
|
||||
export const webhookRoutes = new Router()
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { config } from '../config'
|
||||
import { config } from '../../config'
|
||||
|
||||
const UPSTREAM = config.upstream.replace(/\/$/, '')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// Mock hermes-cli
|
||||
vi.mock('../../packages/server/src/services/hermes-cli', () => ({
|
||||
vi.mock('../../packages/server/src/services/hermes/hermes-cli', () => ({
|
||||
listProfiles: vi.fn(),
|
||||
getProfile: vi.fn(),
|
||||
createProfile: vi.fn(),
|
||||
@@ -16,7 +16,7 @@ vi.mock('../../packages/server/src/services/hermes-cli', () => ({
|
||||
importProfile: vi.fn(),
|
||||
}))
|
||||
|
||||
import * as hermesCli from '../../packages/server/src/services/hermes-cli'
|
||||
import * as hermesCli from '../../packages/server/src/services/hermes/hermes-cli'
|
||||
|
||||
describe('Profile Routes', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -120,7 +120,15 @@ describe('Proxy Handler', () => {
|
||||
})
|
||||
|
||||
it('returns 502 on connection failure', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('ECONNREFUSED'))
|
||||
// waitForGatewayReady loops calling fetch(healthUrl) until res.ok or timeout.
|
||||
// Return ok:true for health checks so the loop exits immediately (gateway
|
||||
// "ready"), then the retry fetch also fails with ECONNREFUSED → 502.
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
if (typeof url === 'string' && url.includes('/health')) {
|
||||
return Promise.resolve({ ok: true })
|
||||
}
|
||||
return Promise.reject(new Error('ECONNREFUSED'))
|
||||
})
|
||||
|
||||
const ctx = createMockCtx()
|
||||
await proxy(ctx)
|
||||
|
||||
Reference in New Issue
Block a user