Skip to content
Merged
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
51 changes: 29 additions & 22 deletions ui/src/views/application/application-card/ApplicationCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,44 @@
import type { ApplicationDetail } from '@/api/types'
import { isWorkFlow } from '@/utils/common'
import { dateFormat } from '@/utils/time'

import applicationAPI from "@/api/admin/workspace/application/application"
import { useRoute, useRouter } from 'vue-router';
import { computed } from 'vue';
defineOptions({ name: 'ApplicationCard' })

const router = useRouter()
const props = defineProps<{ application: ApplicationDetail; selectable?: boolean; selected?: boolean }>()

import { CHAT_API_BASE_PATH } from '@/api/constants'
const emit = defineEmits<{ click: []; selected: [selected: boolean] }>()

defineSlots<{ 'action-dropdown'?: () => unknown }>()
const apiQuery = computed(() => {
const query = props.application?.work_flow?.nodes?.find(n => n.id === 'base-node')?.properties?.api_input_field_list.map((f: any) => {
return `${f.variable}=${f.default_value}`
}).join('&')
if (query) {
return '?' + query
}
return ''

})

const chatBasePath = CHAT_API_BASE_PATH.replace(/\/api$/, '')

function handleOpen() {
if (!props.selectable) emit('click')
}
const toChat = () => {
applicationAPI.getAccessToken(props.application.id).then(ok => {
const accessUrl = ok.access_token ? `${window.location.origin}${chatBasePath}/${ok.access_token}/${apiQuery.value}` : ''
window.open(accessUrl, '_blank')
})
}
</script>

<template>
<MkResourceCard
:create_time="application.create_time"
:nick_name="application.nick_name || '-'"
:selectable="selectable"
:selected="selected"
:title="application.name"
:class="{ 'cursor-pointer': !selectable }"
@click="handleOpen"
@selected="emit('selected', $event)"
>
<MkResourceCard :create_time="application.create_time" :nick_name="application.nick_name || '-'"
:selectable="selectable" :selected="selected" :title="application.name" :class="{ 'cursor-pointer': !selectable }"
@click="handleOpen" @selected="emit('selected', $event)">
<template #icon>
<ApplicationIcon :icon="application.icon" />
</template>
Expand All @@ -42,14 +55,8 @@ function handleOpen() {

<template #footer="{ Action, ActionDropdown }">
<MkStatusLabel v-if="application.is_portal" type="custom" label="已上架" icon="icon_launch" class="text-N600!" />
<MkStatusLabel
v-else
:active="application.is_publish"
active-text="已发布"
inactive-text="未发布"
:icon="application.is_publish ? undefined : 'icon_time_filled'"
class="text-N600!"
/>
<MkStatusLabel v-else :active="application.is_publish" active-text="已发布" inactive-text="未发布"
:icon="application.is_publish ? undefined : 'icon_time_filled'" class="text-N600!" />
<template v-if="application.is_publish">
<el-divider direction="vertical" />
<span class="flex-align-center gap-2">
Expand All @@ -61,7 +68,7 @@ function handleOpen() {
<component :is="Action">
<!-- 去对话 -->
<MkTooltip content="去对话" placement="top">
<el-button text>
<el-button text @click="toChat">
<MkIcon name="icon_new-chat_outlined" />
</el-button>
</MkTooltip>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

🔴 Bugs / will break at runtime

  1. Unsafe optional chain → TypeError
properties?.api_input_field_list.map(...)

The ?. chain guards up to api_input_field_list, but .map is called directly on it. If api_input_field_list is undefined (very common), this throws inside a computed and breaks rendering of the whole card list.

const list = props.application?.work_flow?.nodes?.find(n => n.id === 'base-node')?.properties?.api_input_field_list ?? []
  1. Empty URL opens a blank tab
const accessUrl = ok.access_token ? `...` : ''
window.open(accessUrl, '_blank')

When the token is missing you call window.open(''), which opens an empty tab. Guard before opening (and ideally show an error/toast).

  1. el-button click bubbles to the card click handler
    The card root has @click="handleOpen", so clicking the chat button also fires the card click (emitting click, potentially navigating). Use @click.stop="toChat".

  2. Malformed URL when there is no query

`${...}/${ok.access_token}/${apiQuery.value}`

With apiQuery === '' you get a trailing slash .../token/; with a query you get .../token/?a=b. The slash before an optional query is almost certainly unintended:

`${window.location.origin}${chatBasePath}/${ok.access_token}${apiQuery.value}`

🟠 Security / robustness

  1. Reverse tabnabbing — always pass noopener,noreferrer:
window.open(url, '_blank', 'noopener,noreferrer')
  1. No error handling / no loading state — .then without .catch gives an unhandled rejection; rapid clicks fire multiple token requests. Prefer async/await + try/catch + a loading ref that disables the button.

  2. No URL encoding — f.variable / f.default_value can contain &, =, spaces, Chinese text, etc.:

return `${encodeURIComponent(f.variable)}=${encodeURIComponent(f.default_value ?? '')}`
  1. Access token in the URL path is exposed in browser history, server logs, and Referer headers. At minimum this is worth a comment/confirmation with the backend; a short-lived token or header-based auth is preferable.

🟡 Code quality / style

  1. Imports are not at the top of the file. import applicationAPI, useRoute/useRouter, computed, and CHAT_API_BASE_PATH are declared after defineOptions/defineProps/defineEmits. This violates import/first, hurts readability and may break some tooling. Consolidate all imports at the top and merge duplicates (vue, vue-router).

  2. Unused import — useRoute is never used.

  3. (f: any) — give it a proper type (ApiInputField), and type the computed's return as string.

  4. Simplification:

const apiQuery = computed(() => {
  const fields = props.application?.work_flow?.nodes
    ?.find(n => n.id === 'base-node')?.properties?.api_input_field_list ?? []
  const query = fields.map(...).join('&')
  return query ? `?${query}` : ''
})
  1. chatBasePath doesn't depend on reactive state — move it to module scope so it isn't recomputed per instance. Also CHAT_API_BASE_PATH.replace(/\/api$/, '') silently does nothing if the constant has a trailing slash (/api/); consider normalizing or asserting.

  2. Business logic leaking into a presentational card. ApplicationCard is now coupled to work_flow internals and the application API. Extract into a composable, e.g. useApplicationChat(application) returning { apiQuery, openChat, loading }.

  3. Double guard mismatch — :class="{ 'cursor-pointer': !selectable }" implies the card is clickable when not selectable, but handleOpen only emits when !selectable. Correct, but the toChat button is rendered unconditionally in the footer; consider hiding/disabling it when !application.is_publish (the sibling v-if block implies unpublished apps shouldn't be chatted with).

  4. Diff noise — the re-formatting of MkResourceCard and MkStatusLabel (re-wrapping attributes) is unrelated to the feature and makes review harder. Revert it or split into a separate commit.

Suggested rewrite (core part)

import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import applicationAPI from '@/api/admin/workspace/application/application'
import { CHAT_API_BASE_PATH } from '@/api/constants'

const CHAT_BASE_PATH = CHAT_API_BASE_PATH.replace(/\/api\/?$/, '')
const router = useRouter()

const apiQuery = computed(() => {
  const fields =
    props.application?.work_flow?.nodes
      ?.find(n => n.id === 'base-node')
      ?.properties?.api_input_field_list ?? []

  const query = fields
    .map(f => `${encodeURIComponent(f.variable)}=${encodeURIComponent(f.default_value ?? '')}`)
    .join('&')

  return query ? `?${query}` : ''
})

const chatLoading = ref(false)
async function toChat() {
  if (chatLoading.value) return
  chatLoading.value = true
  try {
    const { access_token } = await applicationAPI.getAccessToken(props.application.id)
    if (!access_token) {
      // ElMessage.error('获取访问令牌失败')
      return
    }
    const url = `${window.location.origin}${CHAT_BASE_PATH}/${access_token}${apiQuery.value}`
    window.open(url, '_blank', 'noopener,noreferrer')
  } catch (e) {
    // ElMessage.error('打开对话失败')
  } finally {
    chatLoading.value = false
  }
}
<el-button text :loading="chatLoading" @click.stop="toChat">

Expand Down
Loading