手把手编写儿童手机远程监控App之Copilot到Agent——我的开发工作流正在被颠覆

0 阅读7分钟

概述

上节完成在嘟宝项目中使用高德api,如何在地图上显示gps位置。这节将使用Copilot演示在地图山画围栏、曲线。 嘟宝是为监督孩子身边环境而创建的一套应用,它能够实现后台驻留长连接,随时接收嘟妈信令,建立音视频通道点对点通信,而不需要中转服务器。嘟妈是一套WEB 应用,使用vue3编写,能够兼容app、小程序开发 。而嘟宝采用原生Andorid开发,能够获取最大权限,保持在后台运行。 目前为止,嘟宝完成的基本功能设计包括

  • MQTT 基础通信
  • 后台驻留
  • SQLite数据存储
  • 二维码显示身份识别码
  • 开启自启动前台服务
  • 任务栏消息发送与点击
  • 高德定位实时更新
  • webrtc点对点通信
  • mAgentWeb控件封装解决全屏与获取视频权限问题 嘟妈采用vue3编写它的核心需求已完成包含以下部分:
  • 完成登录注册页面
  • 完成二维码扫描完成与嘟宝绑定
  • 与嘟妈建立音视频通信
  • 查看嘟宝桌面共享视频 项目地址在github上。 嘟宝地址 嘟妈地址
    地图上划线
    //详情如何引入使用高德api,参考上节
function polyline(gpsData: any) {
  gpsData = [
    [116.397428, 39.90923], // 起点:天安门
    [116.401234, 39.912345],
    [116.403322, 39.915119], // 故宫
    [116.407654, 39.91789],
    [116.410691, 39.902737], // 王府井
    [116.413456, 39.905678],
    [116.416789, 39.908901], // 终点
  ]
  const polyline = new AMap.Polyline({
    path: gpsData,
    strokeColor: '#3366FF',
    strokeWidth: 6,
    strokeOpacity: 0.8,
  })
  map.value.add(polyline)
  map.value.setFitView([polyline])

  const startMarker = new AMap.Marker({
    position: gpsData[0],
    title: '起点',
    label: {
      content:
        '<div style="background:#4CAF50;color:#fff;padding:2px 10px;border-radius:12px;">起点</div>',
      direction: 'top',
    },
  })
  map.value.add(startMarker)
  const endMarker = new AMap.Marker({
    position: gpsData[gpsData.length - 1],
    title: '终点',
    label: {
      content:
        '<div style="background:#F44336;color:#fff;padding:2px 10px;border-radius:12px;">终点</div>',
      direction: 'top',
    },
  })
  map.value.add(endMarker)
}

地图上画围栏

该功能,使用Copilot完成,

实现功能如下:

gaode.vue源码

<template>
  <div class="map-page">
    <button class="back-btn" type="button" @click="goBack">
      <van-icon name="arrow-left" size="18" />
    </button>
    <button class="more-btn" type="button" @click="goMore">
      <van-icon name="ellipsis" size="25" />
    </button>
    <div id="container"></div>
    <van-action-sheet
      v-model:show="showMenu"
      :actions="menuActions"
      cancel-text="取消"
      @select="onMenuSelect"
    />
    <van-popup v-model:show="showHistoryPicker" round position="bottom">
      <div class="history-picker">
        <div class="history-picker-title">选择历史时间</div>
        <div class="history-picker-row">
          <label class="history-picker-label">开始时间</label>
          <input class="history-picker-input" type="datetime-local" v-model="historyStart" />
        </div>
        <div class="history-picker-row">
          <label class="history-picker-label">结束时间</label>
          <input class="history-picker-input" type="datetime-local" v-model="historyEnd" />
        </div>
        <div class="history-picker-actions">
          <van-button size="small" plain type="default" @click="showHistoryPicker = false">
            取消
          </van-button>
          <van-button size="small" type="primary" @click="onHistoryQuery"> 查询 </van-button>
        </div>
      </div>
    </van-popup>
    <van-popup v-model:show="showFencePicker" round position="bottom">
      <div class="history-picker">
        <div class="history-picker-title">设置电子围栏</div>
        <div class="history-picker-row">
          <label class="history-picker-label">半径(米)</label>
          <input
            class="history-picker-input"
            type="range"
            min="50"
            max="2000"
            step="10"
            v-model.number="fenceRadius"
          />
          <div style="width: 60px; text-align: right">{{ fenceRadius }} m</div>
        </div>
        <div class="history-picker-row">
          <div style="color: #666; font-size: 13px">
            点击地图设置围栏中心,或再次点击地图移动中心。
          </div>
        </div>
        <div class="history-picker-actions">
          <van-button
            size="small"
            plain
            type="default"
            @click="((showFencePicker = false), clearFenceOverlay())"
          >
            取消
          </van-button>
          <van-button size="small" type="primary" @click="saveFence"> 保存 </van-button>
        </div>
      </div>
    </van-popup>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, shallowRef, ref, watchEffect } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import AMapLoader from '@amap/amap-jsapi-loader'
import { showToast } from 'vant'
import { c } from '@/myconfig'
import { useMQTTStore } from '@/store/mqtt'
import { useDubaoStore } from '@/store/dubao'
import 'vant/es/toast/style'
import { watch } from 'fs'

const mqtt = useMQTTStore()
const dubao = useDubaoStore()
const showMenu = ref(false)
const showHistoryPicker = ref(false)
const historyStart = ref(formatDateTimeLocal(new Date(Date.now() - 1000 * 60 * 60 * 24 * 2)))
const historyEnd = ref(formatDateTimeLocal(new Date()))
// geofence state (top-level)
const showFencePicker = ref(false)
const fenceCenter = ref<[number, number] | null>(null)
const fenceRadius = ref(200) // meters
let fenceMarker: any = null
let fenceCircle: any = null
const menuActions = [
  { name: '定位', icon: 'aim', action: 'site' },
  { name: '运动轨迹', icon: 'guide-o', action: 'sport' },
  { name: '历史位置', icon: 'underway-o', action: 'his' },
  { name: '电子围栏', icon: 'notes-o', action: 'fence' },
]
function onMenuSelect(action: any) {
  showMenu.value = false
  switch (action.action) {
    case 'site':
      mqtt.pushmsg(dubaoId as string, 'gps', JSON.stringify({ page: 1, rows: 1 }))
      break
    case 'sport':
      let stop = Date.now()
      let start = stop - 1000 * 60 * 60 * 24 * 1
      mqtt.pushmsg(
        dubaoId as string,
        'gpssport',
        JSON.stringify({ page: 1, rows: 100, start: start, stop: stop }),
      )
      break
    case 'his':
      showHistoryPicker.value = true
      break
    case 'fence':
      showFencePicker.value = true
      break
    default:
      break
  }
}

function formatDateTimeLocal(date: Date) {
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  const hour = String(date.getHours()).padStart(2, '0')
  const minute = String(date.getMinutes()).padStart(2, '0')
  return `${year}-${month}-${day}T${hour}:${minute}`
}

function onHistoryQuery() {
  if (!historyStart.value || !historyEnd.value) {
    showToast('请选择开始和结束时间')
    return
  }
  const start = new Date(historyStart.value).getTime()
  const stop = new Date(historyEnd.value).getTime()
  if (Number.isNaN(start) || Number.isNaN(stop)) {
    showToast('时间格式不正确')
    return
  }
  if (start >= stop) {
    showToast('结束时间必须晚于开始时间')
    return
  }
  showHistoryPicker.value = false
  console.log('查询历史轨迹:', start, stop)
  mqtt.pushmsg(
    dubaoId as string,
    'gpssport',
    JSON.stringify({ page: 1, rows: 100000, start, stop }),
  )
}

// --- geofence helpers ---
function clearFenceOverlay() {
  try {
    if (fenceMarker) {
      map.value.remove(fenceMarker)
      fenceMarker = null
    }
    if (fenceCircle) {
      map.value.remove(fenceCircle)
      fenceCircle = null
    }
  } catch (e) {
    console.error('clearFenceOverlay err', e)
  }
}

function drawFence(center: [number, number], radius: number) {
  if (!map.value || !AMap) return
  clearFenceOverlay()
  fenceMarker = new AMap.Marker({ position: center, title: '围栏中心' })
  fenceCircle = new AMap.Circle({
    center: center,
    radius: radius,
    strokeColor: '#FF5722',
    strokeOpacity: 0.6,
    strokeWeight: 2,
    fillColor: 'rgba(255,87,34,0.15)',
  })
  map.value.add(fenceCircle)
  map.value.add(fenceMarker)
  map.value.setFitView([fenceCircle])
}

function saveFence() {
  if (!fenceCenter.value) {
    showToast('请先在地图上选择围栏中心点')
    return
  }
  const [lng, lat] = fenceCenter.value
  const data = { lng, lat, radius: fenceRadius.value }
  console.log('保存围栏数据:', data)
  mqtt.pushmsg(dubaoId as string, 'setfence', JSON.stringify(data))
  showFencePicker.value = false
  showToast('围栏已保存')
}

mqtt.$subscribe(async (mutate: any, state) => {
  if (typeof mutate.events.newValue === 'boolean') {
    return
  }
  let data = JSON.parse(state.datamsg)
  let msg = JSON.parse(data.msg)
  switch (msg.code) {
    case 'gps':
      const gpsData = JSON.parse(msg.data)
      maker(gpsData[0])
      break
    case 'gpssport':
      console.log('gpssport', msg.data)
      if (!msg.data || msg.data === '[]') {
        showToast('没有运动轨迹数据')
        console.log('没有运动轨迹数据')
        return
      }
      const gpssportData = JSON.parse(msg.data)
      polylinesport(gpssportData)
      break
    default:
      break
  }
})
;(window as any)._AMapSecurityConfig = {
  securityJsCode: c.gaodesavekey,
}
let AMap: any
const map = shallowRef<any>(null) // 方式1:使用 any

const router = useRouter()
const route = useRoute()
const dubaoId = route.params.dubaoId

const goBack = () => {
  router.back()
}

const goMore = () => {
  showMenu.value = true
}
watchEffect(() => {
  console.log('fenceCenter changed:', fenceCenter.value)
  drawFence(fenceCenter.value as [number, number], fenceRadius.value)
})
onMounted(() => {
  AMapLoader.load({
    key: c.gaodekey,
    version: '2.0',
    plugins: ['AMap.ToolBar', 'AMap.Scale'],
  })
    .then((AMap2) => {
      AMap = AMap2
      map.value = new AMap.Map('container', {
        viewMode: '2D',
        zoom: 14,
        center: [116.397428, 39.90923],
      })

      map?.value.addControl(new AMap.ToolBar())
      map?.value.addControl(new AMap.Scale())
      // map click: set fence center when fence picker open
      map.value.on('click', (e: any) => {
        console.log('Fence center set to:', e)
        // if (!showFencePicker.value) return
        const lnglat = e.lnglat || e.latlng
        const center: [number, number] = [lnglat.lng, lnglat.lat]
        fenceCenter.value = center
        drawFence(center, fenceRadius.value)
        console.log('Fence center set to:', center)
      })
      let stop = Date.now()
      let start = stop - 1000 * 60 * 60 * 24 * 7
      mqtt.pushmsg(
        dubaoId as string,
        'gps',
        JSON.stringify({ page: 1, rows: 1, start: start, stop: stop }),
      )
      // maker({ lng: 117.397428, lat: 39.90923, name: '嘟宝' })
    })
    .catch((e) => {
      console.error('地图加载失败:', e)
    })
})

onUnmounted(() => {
  map.value?.destroy()
})
function maker(gps: any) {
  if (!map.value) return
  let name = dubao.getDuBao(dubaoId as string)?.dubaoName || '嘟宝'
  const marker = new AMap.Marker({
    position: new AMap.LngLat(gps.lng, gps.lat),
    title: name,
    animation: 'AMAP_ANIMATION_DROP', // 落点动画
  })
  marker.on('click', (e: any) => {
    const infoWindow = new AMap.InfoWindow({
      content: `<div style="padding: 5px;">${name}</div>`,
      offset: new AMap.Pixel(0, -30),
    })
    infoWindow.open(map.value, marker.getPosition())
    console.log('Marker clicked:', name, e)
  })
  // 添加到地图

  map.value.add(marker)
  map.value.setCenter(marker.getPosition())
}
function polyline(gpsData: any) {
  gpsData = [
    [116.397428, 39.90923], // 起点:天安门
    [116.401234, 39.912345],
    [116.403322, 39.915119], // 故宫
    [116.407654, 39.91789],
    [116.410691, 39.902737], // 王府井
    [116.413456, 39.905678],
    [116.416789, 39.908901], // 终点
  ]
  const polyline = new AMap.Polyline({
    path: gpsData,
    strokeColor: '#3366FF',
    strokeWidth: 6,
    strokeOpacity: 0.8,
  })
  map.value.add(polyline)
  map.value.setFitView([polyline])

  const startMarker = new AMap.Marker({
    position: gpsData[0],
    title: '起点',
    label: {
      content:
        '<div style="background:#4CAF50;color:#fff;padding:2px 10px;border-radius:12px;">起点</div>',
      direction: 'top',
    },
  })
  map.value.add(startMarker)
  const endMarker = new AMap.Marker({
    position: gpsData[gpsData.length - 1],
    title: '终点',
    label: {
      content:
        '<div style="background:#F44336;color:#fff;padding:2px 10px;border-radius:12px;">终点</div>',
      direction: 'top',
    },
  })
  map.value.add(endMarker)
}
function polylinesport(gpsData: any) {
  console.log('polylinesport', gpsData)
  let gpsDataArray: any[] = []
  for (let i = 0; i < gpsData.length; i++) {
    gpsDataArray[i] = [gpsData[i].lng, gpsData[i].lat]
  }
  const polyline = new AMap.Polyline({
    path: gpsDataArray,
    strokeColor: '#3366FF',
    strokeWidth: 6,
    strokeOpacity: 0.8,
  })
  map.value.add(polyline)
  map.value.setFitView([polyline])

  const startMarker = new AMap.Marker({
    position: gpsDataArray[gpsDataArray.length - 1],
    title: '起点',
    label: {
      content:
        '<div style="background:#4CAF50;color:#fff;padding:2px 10px;border-radius:12px;">起点</div>',
      direction: 'top',
    },
  })
  map.value.add(startMarker)
  const endMarker = new AMap.Marker({
    position: gpsDataArray[0],
    title: '终点',
    label: {
      content:
        '<div style="background:#F44336;color:#fff;padding:2px 10px;border-radius:12px;">终点</div>',
      direction: 'top',
    },
  })
  map.value.add(endMarker)
}
</script>

<style scoped>
.map-page {
  position: relative;
  width: 100%;
  height: 100%;
}

#container {
  width: 100%;
  height: 100%;
}

.back-btn {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.95);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  color: #333;
}
.more-btn {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.95);
  /* background: transparent; */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  color: #333;
}
.history-picker {
  padding: 16px 16px 20px;
}
.history-picker-title {
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 12px;
}
.history-picker-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.history-picker-label {
  font-size: 14px;
  color: #333;
}
.history-picker-input {
  width: 60%;
  border: 1px solid #e5e5e5;
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 14px;
}
.history-picker-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 16px;
}
</style>

引言:AI辅助开发的演进之路

  • 从代码补全到智能助手:Copilot如何改变编程习惯
  • 从助手到伙伴:Agent带来的范式转变
  • 本文主旨:分享个人开发工作流被AI重塑的亲身经历

第一部分:Copilot时代——效率的第一次飞跃

1.1 从IntelliSense到GitHub Copilot

  • 传统IDE智能提示的局限性
  • Copilot带来的上下文感知代码生成
  • 实际案例:如何用Copilot加速日常开发

1.2 Copilot工作流整合

  • VS Code中的无缝集成体验
  • 代码审查与重构的辅助
  • 学习曲线与适应过程

1.3 Copilot的局限与瓶颈

  • 上下文窗口的限制
  • 复杂业务逻辑的挑战
  • 对开发者思维模式的依赖

第二部分:Agent登场——从工具到合作伙伴

2.1 什么是AI Agent?

  • Agent与Copilot的本质区别
  • 自主决策与任务分解能力
  • 记忆与学习机制

2.2 主流开发Agent体验

  • Cursor Agent模式深度体验
  • Windsurf的智能工作流
  • 其他开发Agent工具对比

2.3 Agent如何重构开发流程

  • 需求分析到代码实现的闭环
  • 自动化测试与调试
  • 文档生成与知识管理

第三部分:我的工作流颠覆实录

3.1 需求阶段:从模糊描述到清晰规划

  • 自然语言需求直接转化为技术方案
  • Agent协助的技术选型与架构设计
  • 风险评估与备选方案生成

3.2 开发阶段:从写代码到指导代码

  • 角色转变:从执行者到架构师
  • Agent驱动的迭代开发模式
  • 实时代码审查与优化建议

3.3 测试与部署:自动化程度的质变

  • 智能测试用例生成
  • 性能分析与优化建议
  • 部署配置与监控设置

第四部分:技术深度解析

4.1 底层技术对比

  • Copilot:基于大型代码库的补全模型
  • Agent:多模态理解与规划能力
  • RAG与工具调用机制

4.2 实际效能数据

  • 开发速度提升对比
  • 代码质量指标变化
  • Bug率与维护成本统计

4.3 集成开发环境演进

  • IDE插件的智能化升级
  • 命令行工具的AI增强
  • 团队协作平台的变化

第五部分:挑战与应对策略

5.1 技术挑战

  • 上下文管理的复杂性
  • 幻觉问题与准确性保障
  • 私有代码的安全考量

5.2 工作习惯调整

  • 思维模式的转变过程
  • 与Agent的有效协作技巧
  • 保持技术深度的平衡

5.3 团队协作变革

  • 代码审查流程的重构
  • 知识共享方式的进化
  • 项目管理工具的适配

第六部分:未来展望

6.1 技术发展趋势

  • 多Agent协作系统
  • 领域专用Agent的兴起
  • 硬件加速与边缘部署

6.2 开发者角色演变

  • 从编码到“导演”
  • 架构设计能力的强化
  • 业务理解深度的要求

6.3 给开发者的建议

  • 如何开始Agent之旅
  • 技能栈的调整方向
  • 保持竞争力的学习路径

结语:拥抱变革,重塑价值

  • AI不是替代,而是增强
  • 开发工作流的持续进化
  • 在技术浪潮中定位自己的独特价值